Under the Hood
Wasm

Beyond the browser: WASI and the component model

Nothing about the stack machine, linear memory, or the capability-based sandbox actually requires a browser, and this lesson traces the two pieces of infrastructure — WASI's portable syscalls and the component model's typed interfaces — that turn that same execution model into a universal, sandboxed compute target running on servers, at the edge, and as plugins.

Beyond the browser: WASI and the component model

Look back at everything this module has built up: a stack machine that computes, linear memory that stores, a toolchain that produces the bytecode, a compiler pipeline that validates and runs it fast, a sandbox that only lets it touch what it's explicitly given. Notice that not one piece of that description mentioned a browser, a DOM, or JavaScript by necessity. Those are properties of the module and the engine that runs it — the same module, running in the same way, could just as easily be sitting on a server, at a CDN edge node, or loaded as a plugin inside some other application. That's not a hypothetical extension of WebAssembly; it's the direction the ecosystem has actually moved, and it turns out the browser was really just the first host, not the only one WASM was built for.

Getting there mechanically took two pieces of infrastructure this lesson covers in turn: WASI, a portable set of host imports that gives a WASM module a standard way to ask for files, clocks, randomness, and network access without the browser or its APIs in the picture, and the component model, a way for WASM modules written in different languages to exchange rich, typed data with each other instead of just raw numbers.

The insight that makes "outside the browser" work at all

Recall the capability model from the streaming compilation lesson: a WASM module has no ambient authority. It can compute against its own linear memory, and it can call whatever functions its host handed it through imports — nothing else, no exceptions. In the browser, that host is JavaScript, and the imports it hands over are things like a logging callback or a canvas-drawing function.

But notice that description never actually depended on the host being a browser. Swap "JavaScript running in a browser tab" for "a standalone runtime running in a Docker container" or "a plugin host embedded inside a database," and the same guarantee holds unchanged: the module can only do what the host explicitly wired up, and nothing it wasn't given. A portable, sandboxed unit of compute that does exactly what its imports allow and nothing more is exactly what you want for running someone else's code on your server, at the edge, or as a plugin — arguably more than you want it in a browser tab, where the page you're visiting is at least nominally accountable to the site that served it. The browser just happened to be where this idea got proven out first.

WASI: a standard syscall API, capability by capability

The catch is that "call whatever functions the host gave you" only gets you portable code if there's an agreed-upon list of which functions a host is expected to provide. Without one, every server-side WASM runtime would invent its own bespoke set of imports for "read a file" or "get the time," and a .wasm module built against one runtime's imports wouldn't run on another's — you'd be back to writing host-specific glue for every target, the exact problem WASM was supposed to solve.

WASI — the WebAssembly System Interface — is that agreed-upon list: a standardized set of imports covering the things a normal program expects from its operating system — opening and reading files, getting the current time, generating random bytes, reading and writing standard input and output, opening network sockets. A .wasm module compiled against WASI can run unmodified on any runtime that implements WASI, the same way a POSIX-conforming program can move between Unix-like operating systems without being recompiled against a different API. The module doesn't know or care whether it's running under Wasmtime on a laptop or inside some cloud provider's edge network; the imports look the same either way.

The part worth sitting with is how WASI grants access, because it's a genuine departure from how a normal operating system process works. A regular process, once it starts, has ambient authority: it can attempt to open any path on the filesystem it has permission bits for, connect to any address on the network, and the operating system checks permissions at the point of the call. A WASI module has no such default reach. To read a file, it doesn't get "the filesystem" — the host that instantiates it decides, at instantiation time, exactly which directories (if any) to hand over as capabilities, typically as a preopened directory handle. The module can only see and touch the specific directory (or directories) it was explicitly given; there is no path it can construct, no ../../etc/passwd it can walk, that reaches outside what the host preopened, because the module was never handed a capability that reaches there in the first place. The same logic applies to sockets, environment variables, and clocks — each is a specific import the host chooses to provide or withhold, not a blanket ambient permission the module starts with and the host has to actively restrict.

The runtimes that make this real

WASI is a specification, not a program; running it requires a runtime that implements those imports and executes the module. Wasmtime, Wasmer, and WasmEdge are the runtimes doing that work outside the browser today — standalone programs (or embeddable libraries) that load a .wasm file, wire up whichever WASI capabilities the caller configures, validate and compile the module using the same kind of pipeline the previous lesson described for browsers, and run it. None of them are browsers, none of them have a DOM or a JavaScript engine sitting alongside — they exist purely to host WASM as a general-purpose compute target.

Why this matters: cold starts, sandboxing, and one binary everywhere

Three concrete payoffs fall out of running WASM this way instead of a container or a full VM.

Startup is close to instant. Spinning up a container means booting a process inside an isolated namespace, often on top of a virtualized or emulated kernel boundary — real, measurable milliseconds to seconds of overhead before your code runs at all. Instantiating a WASM module reuses the same fast validate-and-compile pipeline from the previous lesson: no OS process to fork, no container filesystem to mount, no kernel namespace to set up. For workloads that need to spin up fresh on every request — serverless functions, per-request edge handlers — that difference in cold-start latency is the whole reason to reach for WASM instead of a container in the first place.

The sandbox is strong enough to run code you don't trust. A plugin system, a serverless platform running arbitrary customer code, or a database evaluating a user-defined function all share the same problem: someone else's code needs to run inside your process or your infrastructure, and it needs to be unable to do anything you didn't explicitly allow. WASI's capability model is precisely that guarantee, enforced the same way the browser's sandbox is — by the engine, on every access, not by convention or by trusting the guest not to misbehave.

One binary runs everywhere. A .wasm file compiled once is architecture- and OS-independent in a way a native binary never is — no separate build for x86-64 Linux versus ARM macOS versus a cloud provider's specific kernel. Ship the one binary, and any WASI-conformant runtime on any of those platforms can run it unmodified.

The other problem: modules can only pass numbers

Portability and sandboxing solve where WASM can run, but they don't solve a friction this module already surfaced. The JS/WASM boundary lesson established that a WASM function can only take and return the four numeric types — no strings, no structs, nothing rich — so passing anything more complex means writing it into linear memory and passing an offset, with both sides agreeing by hand on the layout. That's manageable when one side is JavaScript and you're writing the marshaling code yourself. It gets considerably worse when the goal is composing WASM modules with each other — a component built in Rust calling one built in Go, say — because now there's no JavaScript sitting in the middle to write that glue; each module would need to agree, out of band, on exactly how the other lays out every string and struct it passes, for every pair of languages that might ever want to talk to each other.

The component model: typed interfaces, standardized

The component model solves that at the level of the ecosystem rather than one integration at a time. It layers on top of core WASM modules a standard way to describe an interface using WIT (Wasm Interface Types) — a language-neutral way of saying "this component exports a function that takes a string and a list of records and returns a result," using rich types like strings, lists, records, and variants, not just i32s and f64s. A component is a core WASM module (or several) plus that typed interface description; when two components are composed together, the marshaling code that turns a WIT-level string or record into bytes in linear memory and back again is generated from the standard, not hand-written by whoever's gluing the two pieces together.

This is the direct answer to the boundary lesson's pain, generalized: instead of every pair of languages needing its own bespoke serialization code across the linear-memory gap, the component model gives every language toolchain a single standard to target — generate WIT bindings once, and any other component that also speaks WIT can be composed with yours, regardless of what it was written in. A Rust component and a Go component can call each other's exported functions passing genuine strings and structured records, with the actual bytes-in-memory mechanics handled the same way underneath, just no longer something a human has to get right by hand.

Where this goes next

Everything up to this point — the sandbox, WASI, the component model — assumes a single module running on its own, however portably. It says nothing yet about a module doing more than one thing at once, or crunching through large, uniform data faster than one scalar instruction at a time. Threads, SIMD, and where WASM is going closes out this module by taking WASM from a single-threaded, scalar compute unit to one that can genuinely parallelize — and surveys the rest of the roadmap turning it into as complete a compute target as a native one.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. Explain why the browser's capability-based sandbox generalizes to non-browser hosts without needing any new security concept.
  2. What problem does WASI actually solve — what would go wrong if every server-side WASM runtime invented its own imports for files and clocks?
  3. Contrast ambient authority with capability-based access using the preopened-directory-handle example. Why can't a WASI module construct a path that escapes its preopened directory?
  4. Name the three concrete advantages of running untrusted code as a WASI module instead of in a container, and explain the mechanism behind each.
  5. Why does composing a Rust WASM component with a Go WASM component need more than what the JS/WASM boundary lesson's raw-numbers model already provides?
  6. What does a WIT interface description actually add on top of a core WASM module, and who ends up writing the marshaling code once WIT is in place?
  7. A colleague says 'WASI just reimplements POSIX for WebAssembly.' What's right about that comparison, and what's the one security property where it breaks down?