NextTechBlog

Technology, explained properly.

Server hardware, where WebAssembly workloads increasingly run outside the browser

WebAssembly Beyond the Browser: Where Wasm Actually Runs

Shopify runs merchant-written code inside its own checkout pipeline. Not on the merchant’s server, not behind an HTTP call — inside Shopify’s infrastructure, on the hot path of a transaction. In a 2020 engineering post the company said its runtime started a module in about 35 microseconds and executed roughly 100,000 modules a minute, with module execution taking around 100 microseconds each.

That is the pitch for WebAssembly outside the browser, and it is a narrow one. Not “run anything anywhere,” but: take a piece of code you do not trust, start it in microseconds, give it access to nothing you did not explicitly hand over, and kill it when it misbehaves. Containers cannot do the first part quickly enough. Language-level sandboxes cannot do the second part convincingly enough.

Six years into the effort there is real production usage, a finished 3.0 specification, and a system interface that finally got async I/O in mid-2026. There is also a fair amount of abandoned tooling. Both halves are worth knowing about before you bet a project on it.

What Wasm actually is

WebAssembly is a binary instruction format for a stack-based virtual machine. You compile a language like Rust, C, C++, Go, or C# to it, and a runtime either interprets it or compiles it to native machine code. The format is compact, validated before execution, and designed so that validation is cheap and provable.

The name misleads people. Wasm has nothing to do with JavaScript beyond a shared origin story. It is not a JavaScript dialect, it does not compile to JavaScript, and running it does not require a JavaScript engine. Standalone runtimes such as Wasmtime, WasmEdge and Wasmer contain no JavaScript at all.

The differences that matter in practice:

  • Static types. Wasm has four core numeric types and a validated type system. There is no dynamic dispatch on every property access, and no deoptimization cliff when your object shapes change.
  • Linear memory. A module gets one flat, contiguous byte array. Every load and store is bounds-checked against it. A pointer inside a Wasm module is an offset into that array, not a host address.
  • Ahead-of-time compilation. A module can be compiled to native code once and reused, which is why startup can be measured in microseconds rather than milliseconds.
  • Any source language. The format is a compilation target, not a language you write by hand.

The sandbox, and why it is different from a container

Wasm’s isolation is structural rather than administrative. A module cannot form a pointer to host memory, because it has no representation for one. Control flow is structured, so there is no arbitrary jump to an address. Function calls go through a type-checked table. The runtime does not need to trust the code, because the code cannot express the operations that would be dangerous.

On top of that sits a capability model. A plain Wasm module has no filesystem, no network, no clock and no environment variables. It can only call functions the host explicitly imported into it. If you want a module to read /data/input.csv, you hand it a handle to that specific directory. There is no ambient authority to escalate — nothing corresponding to “the process runs as this user, so it can open anything that user can open.”

A container, by contrast, is a normal Linux process with namespaces and cgroups drawn around it. It starts with the full system call interface and you subtract from there using seccomp, AppArmor and capability drops. Subtraction is harder to get right than addition, which is the whole security argument for Wasm.

WASI: the part that took the longest

Core WebAssembly deliberately defines no I/O. The WebAssembly System Interface, WASI, fills that gap, and its history explains a lot of the ecosystem’s churn.

WASI 0.1, usually called Preview 1, was a flat C-style ABI. It worked, most tooling still targets it, and it was never meant to be permanent.

WASI 0.2 was approved by the WASI Subgroup on 25 January 2024 and declared stable. It shipped two “worlds”: wasi-cli, roughly a POSIX subset with files, sockets, clocks and randomness, and wasi-http, built around HTTP requests and responses. More importantly, 0.2 rebased WASI on the component model, which made it cross-language and virtualizable instead of tied to C calling conventions.

WASI 0.3 launched on 11 June 2026 and is the one that fixes the most-complained-about gap: asynchronous I/O. Before 0.3, every component that wanted concurrency had to carry its own event loop, which meant components could not be composed without stacking runtimes. In 0.3 async lives in the component model’s canonical ABI and the host runs a single shared event loop. Wasmtime 46 ships it with async enabled by default, and guest toolchains for Rust, Go, JavaScript, Python and C were in progress at launch.

Two things follow from this timeline. First, “does it support WASI?” is not a yes/no question — always ask which version. Second, if you evaluated server-side Wasm before 2024, you evaluated a different thing.

The component model, in plain terms

A core Wasm module can only pass numbers across its boundary. To pass a string you pass an offset and a length into linear memory, and the caller has to know the encoding, the allocator, and who frees it. Every toolchain invented its own convention, so a Rust module and a Go module could not call each other without glue written by hand.

The component model adds a real type system on top — strings, lists, records, variants, resources — plus an interface description language called WIT. As the Bytecode Alliance documentation puts it, components interact only through declared imports and exports, and cannot export their memory. That last detail closes a side channel: two components sharing a page of memory could otherwise communicate outside their declared contract.

The practical result is that a component written in Rust and one written in Python can be linked together and neither author has to think about the other’s memory layout. A Bytecode Alliance post from June 2026 on the road to Component Model 1.0 describes the model as already heavily used in production while acknowledging remaining work on the ABI, browser implementations, and WIT’s expressivity. It is stable enough to build on and not yet finished.

Where it is actually running

Edge and serverless compute

This was the first commercial use. Fastly built the Lucet runtime specifically for it and published measurements in May 2019: about 52µs to load and instantiate a WASI “hello world,” roughly 30µs per request once one-time setup was done, and under 60µs of total setup and teardown overhead. On that scale, spinning up a fresh sandbox per request is cheaper than keeping a warm pool.

Plugin systems

This is where Wasm has the least competition. Envoy supports Wasm HTTP filters through the proxy-wasm interface, so you can extend a service mesh proxy without rebuilding or restarting it. Shopify Functions let merchants customize discounts and checkout logic. The Scott Logic State of WebAssembly 2023 survey found plugin environments were the fastest-growing non-browser use case, citing the Zellij terminal workspace, the Lapce editor, and Microsoft Flight Simulator add-ons.

The reason is straightforward. If you ship software that other people extend, your options are a scripting language you must sandbox yourself, a native plugin ABI that can crash your process, or an out-of-process service. Wasm gives you in-process speed with out-of-process isolation, and it does not force plugin authors into one language.

Data and query engines

Stream processors and databases have started accepting user-supplied transforms as Wasm modules, for the same reason Shopify did: arbitrary user code needs to run next to the data without the ability to reach the rest of the system. This is the newest of the categories and the least settled.

Performance: two different comparisons

Against native code, Wasm loses, and the honest number is not close to zero. The USENIX ATC 2019 paper Not So Fast: Analyzing the Performance of WebAssembly vs. Native Code by Jangda, Powers, Berger and Guha measured SPEC CPU benchmarks and found average slowdowns of 45% in Firefox and 55% in Chrome, with worst cases of 2.08× and 2.5×. Those were browser engines in 2019 and standalone runtimes have improved since, but the shape of the result holds: expect a meaningful fraction slower, not a rounding error. Bounds checks, indirect call checks and the absence of some low-level intrinsics all cost something.

Against containers, the comparison is not close in Wasm’s favor, but only on one axis. Container cold start is typically measured in hundreds of milliseconds; Wasm instantiation is measured in tens of microseconds. Wasm binaries are commonly a few hundred kilobytes to a few megabytes against container images in the tens or hundreds of megabytes. Memory per instance is a fraction.

Native processContainerWasm module
Start-upMillisecondsHundreds of msTens of microseconds
Isolation basisOS processNamespaces, cgroups, seccompValidated bytecode plus capabilities
Default accessEverything the user can reachSubtract from full syscall surfaceNothing until granted
Compute speedBaselineBaselineSlower, workload-dependent
Artifact sizeSmallTens to hundreds of MBHundreds of KB to a few MB
PortabilityPer OS and CPUPer CPU architectureOne binary everywhere

What a container gives you that Wasm does not is the ability to run existing software unchanged. That is not a small thing.

What is still missing

Threads. The weakest area. Browser Wasm has shared memory and atomics, but shared-everything threading for standalone runtimes is still being worked out at the specification level. WASI 0.3 delivered async I/O, which is a different problem. If your workload needs real shared-memory parallelism inside one module, check current runtime support carefully before committing.

Garbage collection. This was a genuine blocker and is now resolved at the spec level. WebAssembly 3.0 was completed on 17 September 2025 and includes managed GC with struct and array types, alongside 64-bit memory, multiple memories, typed references, tail calls, exception handling and relaxed SIMD. Before this, garbage-collected languages had to ship their own collector inside the module, which bloated binaries badly. Toolchain support for the new features is still catching up.

No DOM access. Inside a browser, Wasm cannot touch the DOM directly. It calls into JavaScript, which touches the DOM. Frameworks hide this, but the boundary crossings are real and they cost. Wasm is for computation, not for replacing your UI layer.

Debugging and observability. Repeatedly the second-most-cited gap in community surveys. Stack traces, profilers and step debuggers across the host/guest boundary are workable but nowhere near the maturity of native tooling.

Ecosystem churn. Worth being blunt about. Docker Desktop’s Wasm workload support was shipped as a headline feature, and the current Docker documentation now marks it beta, deprecated, and scheduled for removal. Runtimes and toolchains in this space have a real failure rate. Pick dependencies with that in mind.

What this means for you

Good fit: you need to run code you did not write; you need per-request isolation with negligible start-up cost; you want one binary that runs across architectures; you are building an extensibility story and do not want to pick your users’ language for them.

Bad fit: you want to replace containers wholesale; your workload is heavily multithreaded; you need mature profilers today; you are looking for raw compute speed rather than isolation. On pure throughput, native wins.

How to try it in an afternoon

  1. Install a standalone runtime. Wasmtime is the Bytecode Alliance reference implementation and the one that tracks the specs most closely.
  2. Compile something small. In Rust, add the wasm32-wasip2 target and build a program that reads a file and prints a result.
  3. Run it with no permissions and watch the file open fail. Then grant exactly one directory and watch it succeed. That is the capability model in about ninety seconds, and it is the part that changes how you think about the technology.
  4. Write a WIT interface and generate bindings for two different languages. This is where the component model stops sounding abstract.

Frequently asked questions

Is WebAssembly going to replace Docker?

No. The categories overlap only for stateless, short-lived, compute-shaped workloads. Anything that expects a normal Linux userland — a Postgres server, a shell pipeline, an off-the-shelf binary — stays in a container. The two coexist; containerd shims exist specifically to run Wasm workloads inside existing orchestration.

Which languages compile to Wasm well?

Rust and C/C++ have the best support and the smallest output. Go compiles but historically produced large binaries. C# and .NET have official support. Python and Ruby generally work by shipping an interpreter compiled to Wasm, which costs size and startup. Rust has topped the language usage question in every Scott Logic state-of-Wasm survey.

Is Wasm slower than JavaScript?

Usually faster for numeric and compute-heavy code, and often not worth it for code that mostly manipulates DOM nodes or strings, because every boundary crossing costs. Measure your actual workload rather than trusting a benchmark of someone else’s.

Can a Wasm module escape its sandbox?

The design prevents the module from expressing an escape, but the runtime itself is software and runtimes have had CVEs. The realistic claim is a much smaller and better-defined attack surface than a container, not an unbreakable one.

Should I wait for the ecosystem to settle?

If Wasm is incidental to your product, waiting is reasonable. If your product’s core problem is running untrusted code fast, there is no better answer available today, and the 0.2 and 0.3 releases mean you are building on stable interfaces rather than moving targets.

Where this lands

WebAssembly outside the browser has stopped being speculative and stopped being universal at the same time. The specification is complete, the system interface is stable and now async, and the component model is doing the cross-language work it was designed for. Meanwhile the “Wasm replaces containers” pitch has quietly retired, and some of the tooling built on it has been withdrawn.

What is left is a very good answer to one hard question: how do you run somebody else’s code, right now, without giving it anything. Edge platforms, service meshes and commerce platforms all had that question and all arrived at the same technology. If you have that question too, Wasm is worth the afternoon. If you do not, it is a technology to keep an eye on rather than adopt.

Sources

Image credit: Photo: Marián Hubinský — CC BY-SA 4.0 (via Wikimedia Commons)

Leave a Reply

Your email address will not be published. Required fields are marked *