WebAssembly Left the Browser Because Safe Beats Fast
WebAssembly escaped the browser and found a real job: running somebody else's code inside your process without trusting it. The comparison everyone reaches for — native speed — is the wrong one. What the format actually sells is a plugin that ships once, runs anywhere, and cannot touch a single thing you did not hand it.
The design is deliberately small. A module is bytecode, not machine code, and it is validated before a single instruction executes: every type is checked, every branch target is in range and correctly typed, every function call goes through a typed index. Control flow is structured — blocks, loops, branches — so there are no computed jumps to arbitrary addresses and no return addresses sitting in writable memory. The core specification is a W3C Recommendation, which matters less for its own sake than because it means the sandbox contract is a published document rather than a vendor's promise.
The Sandbox Is the Product
A WebAssembly module has no ambient authority. There is no instruction that opens a file, resolves a hostname, reads a clock, or sends a packet. Everything a module can do beyond arithmetic happens through imports the embedder chose to provide, and the host defines the types and names of those imports. If you do not link a filesystem, the module cannot see a filesystem — not because a policy denies it, but because the capability does not exist in the address space it was given.
Memory is the other half. A module gets a linear memory: one contiguous, growable buffer that is its own, not the host's heap. Every load and store is bounds-checked against the current size of that buffer, so an out-of-bounds access traps instead of corrupting anything outside. Runtimes make those checks nearly free with guard pages or memory protection keys, but the guarantee is the check, not the optimisation. Combined with structured control flow, that gives you control-flow integrity and memory isolation as properties of the format rather than as features bolted on afterwards.
This is why the sandbox framing beats the speed framing. Native code can be faster, but native code from an untrusted party is a liability you have to audit or containment you have to buy. A Wasm module is a thing you can accept from a customer, a tenant, or a mod author and run in the same process as your server, with a capability list you wrote.
Where the Model Hits Walls
The walls are real and they are mostly about the things the sandbox removed on purpose.
- Threads. Shared memory exists, but only as an explicitly shared linear memory with atomic operations, which in browsers demands cross-origin isolation headers before the engine will allow it. WASI grew thread support later, and it remains the least portable part of the story.
- Garbage collection. For years, a managed language had to ship its own collector inside the module and allocate from linear memory. The WasmGC proposal added struct and array types so the host can collect them, and it is now broadly implemented — but it arrived after most early adopters had already made their bet.
- System calls. WASI is capability-based by design: you get preopened directories, not a root filesystem, and there is no fork, no full signal model, and a deliberately narrow view of sockets and time. Preview 1 is deployed; the component model and its newer interface definitions are the direction of travel, and the two do not interoperate for free.
- Address space. Linear memory is 32-bit, which caps a single module's heap at four gibibytes. Fine for a plugin, awkward for a data tool. The memory64 work exists precisely because people hit it.
None of these are bugs. Each is the price of not being able to reach into the host. The mistake is assuming the model is free of trade-offs because the trade-offs were chosen for you.
The Performance Question, Answered Honestly
For compute-bound code that has been compiled ahead of time, WebAssembly typically lands within tens of percent of native, and sometimes closer. The gap comes from bounds checks the optimiser could not prove away, from indirect calls that had to stay indirect, and from the fact that every interaction with the outside world is a host call rather than a syscall with a familiar ABI. For code that spends its life waiting on the network, the gap is noise.
What actually bites in production is startup, not throughput. JIT-based runtimes pay to compile modules at load, which is why edge platforms stream-compile and cache the machine code, and why ahead-of-time compilers such as Wasmtime or wazero exist for services that want a predictable cold start. A plugin that takes forty milliseconds to instantiate is a fine plugin; four hundred is a design problem, and it is a design problem you can measure in milliseconds rather than guess at.
Where It's Actually Earning Its Keep
The deployments that stuck are the ones where the alternative was worse than a sandbox with rough edges. Edge compute platforms run tenant code at request time and cannot afford a container per request, so they use Wasm as the isolation primitive inside a shared runtime. Proxies use it for filters that third parties write and operators deploy without reading. Application platforms use it for extension points so a customer's function cannot take down the host process. Databases use it for user-defined functions with a per-query time and memory budget.
The common thread is one artifact, many hosts. A Rust extension compiled once runs unmodified in a browser, a Node process, a Go service via wazero, and a CDN edge, with the host deciding what capabilities it gets. That is not a performance story. That is a distribution story, and it is the one that pays.
What I Think
The plugin framing is underrated because it is unglamorous. Everyone wanted WebAssembly to be the new native, and it is not, and it never needed to be. What it is instead is the first serious answer to a question that has plagued software for thirty years: how do I let someone else's code run inside my program without giving them my program? Dynamic linking never solved it, in-process scripting never solved it, and containers solve it at the wrong granularity for a request handler.
So I would stop benchmarking it against C and start asking a different question of every extensibility feature in a product: why is this not a Wasm module? If the answer is threads or a syscall you cannot live without, fine, that is a real reason. If the answer is that the SDK did not exist in 2019, that reason has expired.