Est.

Running WebAssembly Modules at the Edge in Production

Wasm modules start in milliseconds and use a tenth the memory of containers.

Columnist · · 14 min read
Cover illustration for “Running WebAssembly Modules at the Edge in Production”
Edge Computing · September 15, 2026 · 14 min read · 3,155 words

Wasm is a binary instruction format, not a language. Code arrives compiled from Rust, C++, Go, and now, thanks to the GC support in Wasm 3.0, managed languages like Kotlin and Dart. Whatever the source, it lands in the same shape: a stack machine with a specific, narrow set of guardrails, and the guardrails are the entire point.

Start with memory. Each module gets its own linear memory, a contiguous byte array it can read and write within, and nothing outside that array exists as far as the module is concerned. An out-of-bounds access doesn't corrupt the neighbor's data. It traps immediately. A native process doesn't get that courtesy: a buffer overrun can quietly scribble into adjacent memory until something unrelated crashes three requests later, and nobody connects the two events until someone's had a bad afternoon with a debugger.

Control flow gets the same treatment. Wasm has no raw goto and no arbitrary jump targets. Branches can only target enclosing blocks, which closes off the entire category of control-flow-hijack exploits that made native binary exploitation a career path for a certain kind of security researcher. You cannot smash a stack and redirect execution to a gadget chain if the instruction format won't let you jump anywhere except where a block already permitted.

The guardrail that actually matters for a multi-tenant edge platform is the third one: imports and exports are the only door in or out. A module declares up front exactly which host functions it needs. If the embedder, whether that's Wasmtime, V8, or something else, doesn't wire in that import, the module cannot call it. There's no ambient syscall table sitting around waiting to be discovered. This is capability-based security, not permission-based security, and the distinction is not academic. A Node.js process starts with the run of the house: filesystem, network, all of it available by default until someone locks it down after the fact. A Wasm module starts with nothing. The embedder hands over capability one function signature at a time, and anyone treating that as a minor implementation detail hasn't thought hard enough about what "multi-tenant" actually requires.

This technique has a name, software fault isolation, which lets a customer's request-handling code sit next to a competitor's on the same physical machine without either one touching the other's data. The isolate model used by major edge compute platforms built on V8 runs on the same bet. None of this comes free, though: no threading (each isolate runs single-threaded, and the Web Worker API isn't supported), no blocking I/O, no OS-level access of any kind. Everything routes through an explicit host import or a WASI grant, full stop.

One warning for anyone eyeing Node's built-in WASI support as a shortcut: it does not implement full sandboxing guarantees, filesystem access isn't strictly confined to pre-opened directories the way the spec intends, and Node's own maintainers warn against running untrusted Wasm through node:wasi. A sandbox with a hole in the floor is not a sandbox, it's a suggestion.

Why Wasm beats containers for edge cold starts, and where containers still win

Diagram: Wasm vs. Containers: Cold Start and Memory at a Glance. Visualizes: Show a side-by-side magnitude comparison of two runtime metrics — cold start time and memory footprint — between Wasm modules and containers.

Cold start is where the argument gets settled fast, and there isn't much room for debate once the numbers are on the table. A Wasm module starts in 1 to 5 milliseconds. A container takes 50 to 500 milliseconds, sometimes longer depending on the image and whatever orchestration layer sits on top of it. Two orders of magnitude, the entire reason Wasm became the default isolation primitive for per-request serverless work at the edge, is not a rounding error.

Memory footprint tells the same story from a different angle. A Wasm module runs in roughly 1 MB or a bit more of memory. A container needs 10 MB or more before it does anything useful. Multiply either number across a single edge machine trying to hold thousands of tenants ready to fire at once, and the footprint gap is the difference between a machine that can serve a whole region and one that quietly falls over under load.

Rust compiled to Wasm sharpens the case further. A simple API written in Rust and compiled to Wasm lands in the 100 to 500 KB range, against 1 to 5 MB for a bundled JavaScript equivalent. Smaller binaries instantiate faster and propagate faster across the 300-plus edge locations a modern CDN operates. For work bound by processor demand, parsing, cryptography, JSON handling, a compiled Wasm build runs 2 to 10 times faster than an interpreted equivalent, with cold starts down in the 0 to 5 millisecond range. That's a real performance case for reaching for a compiled language over an interpreted worker, provided the workload is actually bound by processor demand rather than something else entirely.

Here's the honest caveat, the one people skip past because it complicates a clean story: for I/O-heavy workers spending most of their time waiting on an external API call, the gap between Rust and JavaScript narrows to the point where a rewrite isn't worth the engineering hours. Wasm's advantage is CPU throughput, not I/O latency, and no amount of enthusiasm for a faster runtime fixes a network round-trip. Benchmark the actual workload before signing anyone up for a rewrite, since skipping that step is how teams burn a sprint chasing a speedup that isn't achievable.

Containers still own the territory Wasm doesn't want anyway: long-running stateful sessions, workloads needing real OS threads, anything requiring blocking I/O that WASI hasn't caught up to yet. Wasm is finding its place alongside containers rather than replacing them wholesale. It's replacing them specifically in the high-frequency, short-lived, multi-tenant slice of the workload map, which happens to be exactly the slice edge computing lives in, so the competition is narrower than the headlines suggest.

The market has already placed its bet. A major CDN provider's 2025 acquisition of a Wasm-based serverless company was a strategic signal that this isolation model is now core infrastructure, not a side wager. And Docker, the company that defined what a container even is for a generation of engineers, added support for Wasmtime and Wasmer runtimes. Support for Wasm workloads in Docker Desktop has since been deprecated, but the fact that it shipped at all tells you exactly where the industry's attention went, even if the product didn't stick the landing.

WASI and the Component Model: the standards layer that determines what your module can actually do

WASI, the WebAssembly System Interface, decides whether a module can touch a filesystem, open a socket, or read the clock. It's evolved fast enough that "WASI support" is a question you now have to ask per platform, per version, never something you can assume and move on.

WASI 0.2, also called Preview 2, shipped in early 2024 and brought the Component Model with it, along with the concept of "worlds": cohesive bundles of interfaces built for a specific domain. There's wasi-cli for command-line style modules, wasi-http for an HTTP proxy world, and TCP/UDP support living as an interface inside wasi-cli rather than as a standalone world of its own. Underneath sits WIT, WebAssembly Interface Types, a typed, capability-scoped API surface that grants access to filesystems, networking, clocks, and cryptography with a precision Linux namespaces or Kubernetes PodSecurityPolicies simply don't match. You're not granting a container broad access and hoping the app behaves itself. You're granting a typed function signature and nothing else, which is a stricter and more honest contract than most container security ever offers.

Enterprise commitment to the standard is uneven, and that unevenness is worth taking seriously rather than smoothing over. Microsoft Azure announced in January 2025 that it was closing its experimental WASI node pool support in Azure Kubernetes Service, a reminder that standards adoption in this space moves backward as often as forward, even as the WASI Subgroup inside the W3C WebAssembly Community Group keeps formalizing the spec underneath it.

The Component Model's real payoff shows up in polyglot systems, and this is the part most people underrate entirely. A Go component can call into a Rust component through a typed interface without either one touching the other's raw memory. They interact only through declared functions and declared types, which means a team composes a system out of modules written in different languages without anyone needing to learn anyone else's memory layout. Try that with a shared library across a cross-language binary interface boundary and see how long the goodwill lasts.

WASI 0.3 landed June 11, 2026, adding native async support to the Component Model, which unblocks high-performance networking that previously needed workarounds nobody enjoyed writing. The remaining pieces, folded into WASI 1.0, are targeted for late 2026 or early 2027. Until async lands fully, modules stay effectively single-threaded and stateless at the network boundary, a real limitation worth planning around now rather than discovering it mid-incident.

Runtimes worth knowing by name: Wasmtime, maintained by the Bytecode Alliance, is the reference implementation of WASI and runs in production at companies including Shopify. Spin, from Fermyon, layers routing, key-value storage, SQLite, and Redis pub/sub on top of Wasm apps. WasmEdge targets edge and IoT specifically, with support for AI inference and socket networking, and sits inside the CNCF as a sandbox project alongside other Wasm projects like WasmCloud, which itself runs on Wasmtime under the hood.

The practical takeaway holds regardless of which runtime gets picked: check what WASI version the target platform actually supports before assuming a capability exists. The spec moves fast enough that a capability available at one edge location this quarter may not exist everywhere yet, and finding that out in production is the expensive way to learn it.

Language toolchain choices and what each costs you at the edge

Wasm is a compilation target, so the real decision is which compilation target to target, not whether to use Wasm." It's "which language do we compile to Wasm," and that choice drags binary size, runtime overhead, and toolchain maturity along behind it whether anyone budgeted for that or not.

Rust is the default answer, and it earns that spot rather than inheriting it by hype. It compiles to wasm32-unknown-unknown for a bare target with no underlying system layer, or wasm32-wasip1 when WASI access is needed, and its memory safety guarantees carry straight through into the sandbox instead of fighting against it. A simple API lands in the 100 to 500 KB range. The catch: there's no Tokio, because there are no OS threads inside Wasm, and no blocking I/O either. Async Rust still works, through wasm-bindgen-futures, but the async model has to get designed up front rather than bolted on after version one breaks in a way nobody can reproduce locally. The build pipeline runs Rust through to Wasm and out through a deploy tool, more moving parts than a plain JavaScript deploy, though worker-build and the official Rust worker templates have sanded most of the sharp edges off by now. One detail trips up newcomers constantly: the crate has to build as cdylib, telling the compiler to produce a C-compatible dynamic library the runtime can load. Skip that setting and the compiler happily produces an executable binary the runtime refuses to load, with an error message that doesn't exactly point at the fix.

Go and JavaScript offer the fastest iteration loop, no compile step required, at the cost of a larger bundled binary, typically 1 to 5 MB, and slower throughput on anything demanding heavy processing. Go has an official SDK on Compute platforms via WASI, though Go's Wasm binaries tend to run larger than Rust's because the Go runtime itself has to travel along inside the bundle. C and C++ get there through Emscripten, a path Figma proved out in production with its C++ renderer compiled to Wasm as far back as 2017, though Emscripten's tooling adds friction when the target is an edge runtime instead of a browser tab, which was the environment it was actually built for.

The newest arrivals are managed languages, and they only just became plausible. Kotlin and Dart turned into genuinely viable Wasm targets with Wasm 3.0's native GC support, because before that, targeting Wasm from a garbage-collected language meant shipping a hand-rolled GC inside your own binary, which blew the size budget before a line of business logic got written.

Toolchain maturity moved fast, and things were remarkably bad only two years earlier. Before 2024, debugging a Wasm module was miserable, the Component Model didn't exist yet, and WASI was a moving target nobody sane wanted to build a business on. By 2026, wasm-pack, cargo-component, and wit-bindgen are stable enough to build on without flinching, at least for Rust; other languages are still catching up to that bar. wasm-pack and cargo-component solve different problems, and picking the wrong one wastes an afternoon: wasm-pack targets the browser, wiring up JS interop through wasm-bindgen, while cargo-component targets the WASI Component Model. Reach for the wrong tool and the build produces output for a target nobody in the room asked for.

Binary size and CPU limits: the two platform constraints that break production deployments

Binary size at the edge is a two-front problem, and both fronts bill the same customer: the end user. A bigger binary takes longer to propagate across hundreds of edge locations after every deploy, and it takes longer to instantiate on every single request. Both show up as latency, just at different points in the pipeline, which is exactly why "it's just a slightly bigger binary" is never as small a decision as it sounds.

The toolbox for shrinking a binary is well established at this point, so there's no excuse for skipping it. wasm-opt, part of Binaryen, runs post-compile optimization passes that produce real size reductions without touching a line of source. wasm-snip strips dead code paths no exported function can ever reach. On the Cargo side, tuning the release profile matters more than most people expect: opt-level = "z" optimizes for size over speed, lto = true enables link-time optimization across the whole dependency graph, and codegen-units = 1 trades build parallelism for a smaller, tighter output. Audit dependencies directly, too. Pulling in a full regex engine when a simple string match would do is a classic, entirely avoidable way to bloat a binary for zero functional gain.

CPU limits are the other wall, and they catch people who assume "serverless" means unlimited compute, which it has never meant and never will. Workers platforms typically cap CPU time at 10 milliseconds on the free tier and 50 milliseconds on paid, and that's CPU wall-clock time, not request duration. A worker can sit waiting on a slow upstream API for a full second without penalty. Burn 51 milliseconds of actual CPU cycles, though, and it gets killed mid-request. The workloads that hit this ceiling are exactly the ones that made developers reach for Wasm in the first place: heavy JSON parsing, cryptographic signing, image resizing, regex over large payloads.

That 2 to 10x speedup mentioned earlier translates into faster deploys, lower cloud bills, and snappier cold starts in production. It's a processing budget multiplier with real consequences for pricing tiers. An operation eating the entire 10ms free-tier allowance in JavaScript might cost 1 to 5ms in Rust Wasm, which buys headroom to add more business logic inside the same tier instead of upgrading a plan just to survive a parsing routine that shouldn't have needed the upgrade in the first place.

Threading, again, simply isn't there: one thread per isolate, no Web Worker API, no negotiating around it. Any algorithm designed with parallelism baked in needs a redesign for single-threaded execution, or it needs to move out to a stateful coordination primitive or a queue instead of fighting a runtime with a fixed threading model. SIMD, on the other hand, is supported on Workers, and it's a real lever for numeric and media workloads that can vectorize operations even without multiple threads to spread the work across.

One quieter change worth flagging: a reconfiguration of V8's young-space garbage collection limits in late 2025 delivered close to a 25% improvement on benchmark workloads for a small increase in memory use, and it's already live on Workers. GC used to be a real tax on performance for languages compiled to run alongside this format. It isn't anymore, or at least nowhere near the degree it used to be.

Build the habit of profiling CPU consumption in wrangler dev before shipping, not after. Unit tests never surface a CPU budget burn. Local emulation does, and it's a far cheaper place to catch a 15ms JSON parse than a production incident report is.

State management in stateless sandboxes: the architectural gap between a working handler and a real system

Every Wasm module at the edge starts life with total amnesia. Each request gets a fresh sandbox, freshly instantiated, and nothing survives in memory from one request to the next unless it arrives through an explicit binding to something outside the module. No state persisting means no state leaking between customers, which is the entire basis for multi-tenant safety, so don't mistake the amnesia for a bug.

It is, however, a genuine constraint on application design, and this is where a lot of otherwise solid engineering teams trip. Caches, session data, rate-limit counters, feature flags: none of it exists inside the module, so all of it lives somewhere external, and picking the wrong storage primitive for the job is exactly where "it worked in testing" projects fall apart in production.

Get the mapping wrong and the failure mode is predictable rather than mysterious. A KV store distributed across edge locations is the right call for read-heavy, globally replicated data: configuration values, cached responses, user preferences, anything with a natural TTL. It's eventually consistent, though, which means a write in one region can take a moment to surface as a read in another, and code written with read-after-write assumptions baked in produces bugs that only show up under real geographic traffic, never in a single-region test where the write and the read happen to land on the same box. A relational option like D1's SQLite fits structured queries against data that doesn't change often.

For anything needing strict, single-writer consistency, rate limiting, session state, real-time collaboration, reach for a stateful coordination primitive instead of a distributed cache hoping for the best; it behaves as a single-threaded, strongly consistent actor, which is precisely what those workloads need and a KV store cannot pretend to be. And for work that blows past the CPU budget of a single request, a queue lets that work get offloaded and processed asynchronously instead of forcing it through the same fixed 10 or 50 millisecond window.

None of these primitives are optional extras bolted onto Wasm's execution model after the fact. They're the difference between a handler that returns "hello world" correctly and a system that holds up under the kind of traffic that made someone reach for the edge in the first place.

Sources

  1. WebAssembly: Building Cloud-Native & Edge Apps Beyond the | EM360Tech
  2. Exploring WebAssembly AI Services on Cloudflare Workers
  3. Unpacking Cloudflare Workers CPU Performance Benchmarks
  4. byteiota.com
Filed underEdge Computing

More in Edge Computing