moxzi
Docs / Browser / Browser limits

Browser limitsalpha

What can a tab not do that native can?

Every row below is a decision or a measurement, not an oversight, and each names the source you can check it against. The single constraint underneath most of them: a browser tab is not a replica and cannot be made to look like one without lying, so where a capability is missing it is missing rather than simulated.

The table#

LimitWhy it is this wayWhat it means in practiceCheck
No certificatesA certificate is a subnet threshold signature. There is no subnet and no key, so one could only be forged — worse than none, because a verifier would believe it.ic0.data_certificate_present answers 0 (the same answer an update gets on the IC); data_certificate_size is 0. @dfinity/agent's Actor cannot be used; certified-data patterns need the real IC.moxzi/web/src/lib.rs
Per-instruction metering is opt-inV8 offers no fuel, so the module has to carry its own counter. moxzi-web instruments it at install time; that is a real cost, so it is a decision rather than a default.meter: true at start or per install. Metered, an instruction limit is enforced exactly as moxzid and the IC enforce one. Unmetered, a tight loop runs until wall-clock stops it.moxzi/runtime/src/meter.rs, scripts/web_meter_gate.sh
Wall-clock preemption is worker-only, and coarseA page cannot interrupt a running wasm call, and a terminated worker cannot say where it got to.deadlineMs requires worker: true. Rewind is to the last completed message, so it discards committed work belonging to actors that had nothing to do with the runaway. meter: true is the finer tool: the message traps itself and only its own work is lost, in a page as well as a worker.moxzi/web/lib/moxzi.js, scripts/web_meter_gate.sh
The rewind is coarser than the IC'sA terminated worker cannot report where it got to.A trap on the IC discards exactly its own writes; here the rewind is to the last message that completed. What the runaway itself wrote is gone either way; a rewind crossing an earlier commit point would not be. autosave: true makes the window one message wide.moxzi/web/lib/moxzi.js (recover)
Recovery skips restored-only actorsAn actor that arrived through loadAll has no module bytes on the page's side — only the runtime blob had them — so the page cannot rebuild it after a kill.Post-kill restore covers actors the page installed. For spawned actors, keep your own saveAll checkpoint and loadAll it, as the knights demo does.moxzi/web/lib/moxzi.js (recover)
HTTPS outcalls need a worker and cross-origin isolationfetch is asynchronous and the drain is not, so the runtime blocks on Atomics.wait — forbidden on the main thread — while the page performs the request through a SharedArrayBuffer.http: true requires worker: true plus Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. start refuses immediately if the page is not isolated.moxzi/web/lib/moxzi.js, scripts/web_outcall_gate.sh
An outcall response must fit the shared channelThe response is copied through a fixed SharedArrayBuffer.The channel is 4 MiB and is not configurable through Moxzi.start; a larger response fails with "response too large for the channel".moxzi/web/lib/moxzi.js (sabBytes)
raw_rand is derived, not drawnThe runtime replays a message log to recover, so every external input must be reproducible.Randomness comes from a fixed default seed ([7; 32]) plus a per-caller counter, so two fresh pages produce the same sequence. setSeed exists on the raw WebRuntime but is not exposed through the client library.moxzi/web/src/lib.rs
Deep expression nesting compiles less far than nativelyV8 hosts the guest's stack as well as its own, and a guest frame costs far more there than on wasmtime.Measured: ~60 chained operators compile in a tab; past ~400 terms the limit moves into parsing. Hand-written Motoko does not approach this; machine-generated code should chunk expressions or build with the CLI.docs/limits.md, scripts/web_page_compile_gate.sh
A worker's stack is smaller than a page'sBrowser default, not a setting a page can change.~11,154 JS-frame-equivalents on the main thread versus ~5,303 in a worker. The compiler needs ~8,426, so it runs on the page thread; moving it into a worker makes things worse, not better.scripts/web_page_compile_gate.sh
Many live actors exhaust V8's memoryEach program keeps a memory64 instance alive for its run.Past roughly a hundred simultaneous instances in one isolate, memory.grow fails and the guest traps with a bare unreachable. The corpus gate runs in batches of 30, each its own process, for this reason.scripts/web_corpus_gate.sh
The runtime is closedThere is no agent and no boundary node in a tab.Actors can call actors in the same runtime and can make HTTPS outcalls; they cannot call a canister deployed on the IC. Two tabs are two independent runtimes.moxzi/web/src/lib.rs
Nothing coordinates two tabs over one storeindexedDbStore is a plain {get,set,delete,keys} wrapper with no locking.Two tabs persisting the same actor to the same database will overwrite each other. Use a different store name per tab, or persist from one.moxzi/web/lib/storage.js
Timers only fire when something drives themThe runtime does not own a clock loop.Pass timerMs to Moxzi.start, or call moxzi.tick() yourself. Without either, due timers never run.moxzi/web/lib/moxzi.js
Cycles are accounted, not economicThere is no subnet charging rent.Creation and message fees are charged and observable; idle_cycles_burned_per_day is 0 and nothing is burned while idle.moxzi/web/src/lib.rs
The differential sweep does not cover every corpus programPrograms the browser cannot host are named rather than counted as agreement.149 programs compared, 0 diverging. One (region0-rts-stats) runs on neither host — it asserts the replica's exact physical stable-memory size at canister_init — and is named rather than counted as agreement. Programs that create canisters are counted in their own bucket by the harness.scripts/web_corpus_gate.sh

Two things that are not limits#

Persistence is real. Snapshots use the same layout the native host writes, so a snapshot means the same thing in a page, in moxzid, and in a file. Whole-runtime save/restore brings back spawned actors, whose code never crossed into the page.

Compilation is exact. The in-tab compiler's output is byte-identical to a native moxzi build, including programs with mo: package imports resolved through fetchMopsClosure. This is a byte comparison in the gates, not an assertion that it produced something.

What would change these#

The stack limits are the only rows with a known fix rather than a reason: moving codegen's expression descent onto an explicit heap work-stack — the same change the on-chain campaign already made for the same cause — would stop native frame depth from tracking the input's nesting depth. A frame diet alone buys the compiler headroom and leaves the nesting ceiling where it is.

The rest are structural. Certificates need a subnet, and no amount of instrumentation substitutes for one.

Metering and preemption used to be on that list and are not any more: a module that counts its own instructions needs nothing from the engine, so the browser now traps one runaway message and leaves every other actor alone — the thing terminating a worker could never do.

Next#

On this pageThe tableTwo things that are not limitsWhat would change theseNext