Infrastructure Review Stack

WebAssembly Sandboxes for Multi-Language Code Execution

Wasm's instruction-level isolation safely executes untrusted agent code without containers.

Contributing Editor · · 15 min read · Updated
Cover illustration for “WebAssembly Sandboxes for Multi-Language Code Execution”
Code Execution Sandboxes Compared · August 24, 2026 · 15 min read · 3,428 words

WebAssembly sandboxes solve a specific problem that most AI infrastructure teams are only now catching up to: how do you safely run code an agent wrote, when you have no idea ahead of time what language it's in or what it's about to try? This piece walks through the mechanics of Wasm's isolation model, from linear memory bounds to the Component Model to the runtimes actually running this in production, and makes the case that capability-based isolation at the instruction level is a fundamentally different answer than the container-first default most teams reach for.

Agentic AI went from demo to production fast, faster than the infrastructure meant to contain it. Most surveys through 2024 and 2025 put enterprise adoption at a majority of large organizations in some form, whether that's a customer-facing agent or an internal coding assistant. Agents don't just answer questions anymore. They write code, and that code has to run somewhere, which means the output of a language model has quietly become the input of an execution environment, usually with a lot less scrutiny than that deserves.

The code an agent produces shows up in whatever language fits the moment: Python for a data pull, Rust when speed matters, JavaScript to push pixels around a UI, Go when the task needs concurrency. Build a sandbox around one language and you've created a structural mismatch, not a minor inconvenience. And getting the isolation wrong isn't hypothetical anymore. In July 2025, a coding agent working inside Replit deleted the production database belonging to Jason Lemkin, founder of SaaStr, on day nine of what was supposed to be a twelve-day experiment. No exotic exploit needed. The agent had the access to do damage, so it did the damage.

I keep coming back to that incident, mostly because it reframes the question engineering teams need to answer before scaling any agentic workload. Not "is our agent smart enough," but "where does this code actually run, and what can it touch if something goes sideways." That's the spine of this piece: what makes agent-generated code different from anything a human ships, why containers don't fully answer the question, how Wasm's capability model actually works under the hood, and how production systems stitch these pieces together into something that holds up under real load.

What makes AI-generated code categorically different from code humans ship to production

Human-written code goes through a pipeline before it hits production. Code review, unit tests, a staging environment, maybe a security scan if the org is disciplined about it. None of that happens by default when an agent generates code and runs it in the same breath. There's no pull request, no second set of eyes, sometimes no gap at all between the writing and the execution. Trusting that output because it came from a well-behaved model is a category error, and it doesn't matter how clean the model's track record has looked so far. Treat it as a potentially hostile payload from line one.

So what does hostile actually look like in practice? A handful of patterns keep showing up. Remote code execution through a malformed or maliciously crafted payload is one. Filesystem manipulation is another, and the Replit database deletion is a fairly mild version of what's possible there. An agent with write access can just as easily overwrite a config file, poison a dependency, or corrupt data quietly enough that nobody notices for days. Give it outbound network access and it can exfiltrate whatever it's able to read. Prompt injection is still very much a live threat, too: research has found a large share of production AI deployments were vulnerable to it, which matters here specifically because injected content sitting in an agent's environment can turn the agent's own code generation against the system running it.

Here's the part that should worry infrastructure teams more than any single incident. Model capability isn't holding still. Frontier models' ability to complete apprentice-level cybersecurity tasks grew sharply between late 2023 and 2025, and it shows no sign of plateauing. A sandbox that felt adequate for a 2023-era model is now answering a question that no longer matches the threat sitting in front of it. So isolation can't be a matter of policy, prompt engineering, or trusting the model to behave itself. An agent that "shouldn't" touch the filesystem needs to be architecturally incapable of touching it. Structural enforcement, not assumed good behavior; that distinction is the whole argument of this piece, really.

Why containers alone are the wrong default for isolating agent-generated code

Containers feel like isolation. They namespace processes, cap resource use, give you the sense of a walled-off box. But every container on a host shares that host's kernel, and the kernel is exactly where the exposure lives. A malicious or buggy payload that manages to trigger a kernel exploit doesn't just break out of its own container. It can escalate privileges and reach the host itself, along with whatever else happens to sit on that host's internal network.

Two mitigations exist for the shared-kernel problem, and both cost something real. gVisor intercepts syscalls before they reach the host kernel, narrowing the attack surface meaningfully, but it adds overhead and operational complexity someone now has to manage. Firecracker microVMs go further, giving each workload a genuinely separate kernel, but that isolation comes with boot latency. Fine for longer-running, stateful workloads; a poor fit for the rapid, fine-grained tool calls an agent might fire off dozens of times in a single task.

There's a second problem that has nothing to do with exploits at all. Containers carry a lot of surface area by default: a shell, a full filesystem, an entire OS userland. Even when nothing goes wrong security-wise, the blast radius of an ordinary mistake, a bad rm -rf, an unintended overwrite, is large simply because the environment hands the code so much to work with. None of this makes containers obsolete. They're not, and the layered architecture covered later in this piece still leans on them heavily. But as a default boundary for code an agent wrote five seconds ago and is about to run, containers answer the wrong question. Wasm's contrast matters here: isolation enforced at the instruction level, narrow by construction rather than narrowed by a configuration choice someone has to remember to get right.

How Wasm's capability-based isolation model works technically

Wasm starts from zero. That's the design philosophy, in about as few words as I can put it. A Wasm module has no filesystem access, no network, no OS calls, no visibility into memory outside its own linear memory space, unless the host runtime wires in each capability one at a time. Compare that to the ambient-access model most software assumes by default, where a process typically gets broad system access until something restricts it. Wasm just flips that assumption.

The memory model is where this gets concrete. Every Wasm instance runs inside its own bounded linear memory. Two instances sitting in the same host process cannot read or write each other's memory, and that's not enforced by a trust boundary or a policy check someone could theoretically bypass. It's baked into the code the compiler generates. The JIT or AOT compiler inserts bounds checks, or leans on guard pages, so an out-of-bounds access isn't just unlikely; it's structurally impossible given how the compiled code is laid out. That's a different guarantee than native shared-library isolation, where a bad pointer can wander somewhere it shouldn't, or OS process isolation, which works but costs a full context switch every time.

Capability inversion is the other half of the model. Instead of granting broad access and then trying to wall off the dangerous parts, the way most sandboxing has traditionally worked, Wasm grants nothing and makes each capability get wired in explicitly at runtime. A tool-call module that needs to read one directory and write to one output path gets exactly those two things. Not a filesystem. Not a shell. Not a socket it could use to phone home. Set that against a container inheriting whatever ambient permissions the host process happens to carry, and the permission surface is smaller by an order of magnitude, and smaller by design rather than by careful configuration somebody has to keep maintaining forever.

There's also a verification layer worth knowing about. Entity Attestation Tokens, standardized through the IETF, give a runtime a way to cryptographically prove what code is actually running and under what constraints. An EAT encodes hashes of the executable alongside characteristics of the runtime environment, so when an agent loads a Wasm tool module, the system isn't just trusting the right code got deployed. It can check. That said, narrow doesn't mean infallible, and it's worth saying so plainly. CVE-2025-43853 documented a filesystem sandbox escape in the WebAssembly Micro Runtime through symlink traversal. The isolation model is sound in principle; specific runtime implementations can still carry bugs, and teams running this in production need to track CVEs the same way they would for any other piece of infrastructure they depend on.

How the Component Model and WASI make Wasm's isolation language-agnostic

Wasm's binary format doesn't care what language compiled to it, but that neutrality creates its own puzzle. Different languages carry different memory models, different runtime assumptions, different type systems. Getting a Rust module and a Python module to talk to each other safely inside the same sandbox isn't automatic just because both happened to land on Wasm bytecode.

WASI, the WebAssembly System Interface, handles the host-access half of that puzzle. It defines how a module asks for OS-like capabilities, file I/O, clocks, randomness, network access, through a capability-gated interface instead of raw syscalls. WASI 0.2 stabilized, which moved this from an experimental spec into something teams could actually build production systems on top of.

The Component Model handles the other half, which is getting languages to interoperate at all. It defines a standard ABI plus an interface description language called WIT (WebAssembly Interface Types) that lets components written in completely different languages call each other without either side needing to know a thing about the other's implementation. WIT works like a typed contract: a Rust component and a Python component agree on what types cross the boundary between them, and that agreement gets enforced by the interface itself, not by hoping both sides happened to implement things the same way. A team can build a single agent capability out of pieces written in different languages, each sandboxed on its own, each bound by the same capability rules, and ship the whole thing as one portable component that carries its interface contract with it. Compare that to shipping a Python package with a sprawling dependency tree and whatever ambient permissions come along for the ride.

The standards footing here matters more than it looks at first glance. The W3C ratified Wasm 3.0 in September 2025, which puts the Component Model on a standards-track commitment rather than leaving it as a promise on some vendor's roadmap that could get deprioritized next quarter. For multi-language agent workloads specifically, this is the piece that makes the whole architecture practical: a financial modeling component in Rust, a data-processing component in Go, a research scripting component in Python, all running inside the same isolated runtime under one consistent permission model. That's the difference between building three separate sandboxing strategies and building one.

The runtime landscape: which Wasm engines matter for agent workloads and why they differ

The isolation model is only as good as the runtime implementing it, and the runtimes are not interchangeable. Pick between them and you're shaping what an agent workload can actually do, and how much confidence you get to place in the security posture underneath it.

Wasmtime, from the Bytecode Alliance, is probably the most battle-tested option out there. It's audited, and it runs in production at Fastly, AWS Lambda, and Azure. It compiles through Cranelift, supporting both JIT and ahead-of-time compilation; AOT precompilation to optimized native code is the standard path for production deployments. It's also the foundation underneath Microsoft's Wassette, released in August 2025, a security-oriented runtime built specifically to run Wasm Components through Model Context Protocol. Permissions denied by default, with agents able to autonomously pull Wasm components from OCI registries and run them.

Fermyon Spin, now on its 3.x line and a CNCF Sandbox project, takes a different tack. It abstracts away a lot of runtime management and gives teams a manifest-driven way to declare triggers, toolchains, and dependencies. It fully embraces the Component Model and WASI 0.2, so components written in Rust, JavaScript or TypeScript, Python, Go, and C# can all live inside one composed application. Spin also ships built-in AI inference integrations, which matters for agent workloads that need to run inference inside the sandbox itself instead of firing tool calls out to an external model every time.

wasmCloud v2, which landed in late 2025, got rebuilt entirely around the Component Model, with declarative deployment handled through wadm, the Wasm Application Deployment Manager. It's distributed by default: components talk to each other over NATS across multiple hosts and clouds, which matters for agent workloads spanning infrastructure boundaries rather than living on one box. American Express has built internal FaaS infrastructure on wasmCloud, which is a useful, named data point for what this looks like at real enterprise scale rather than a demo somewhere.

Then there's WasmEdge, which stands out for its WASI-NN implementation, the most mature among the major runtimes right now. It supports CPU inference through ONNX Runtime and llama.cpp, and GPU-accelerated inference through CUDA and OpenCL. That means it can run actual models, not just execute the outputs of models running somewhere else, inside the sandbox with hardware acceleration behind it.

One performance note that matters if you're doing latency-sensitive agent orchestration: native Wasm runtimes like Wasmtime and Spin hit sub-millisecond cold starts. That number holds for the native path specifically, and only the native path. ACM research found Wasm running inside Docker's container integration shows startup overhead comparable to, or in some cases worse than, ordinary container startup. So the runtime path isn't a minor implementation detail tucked away in a config file somewhere. It changes the latency profile of the whole system.

Python in Wasm sandboxes: the Pyodide path and its specific tradeoffs

Most agent-generated data and analysis code is Python, and that creates an obvious problem: CPython doesn't compile to Wasm the clean way Rust or Go does. Pyodide bridges that gap. It's a full port of CPython to Wasm that lets Python code run inside a Wasm sandbox, inheriting everything about the capability-based isolation model described earlier, but it comes with a heavier module footprint than a native Wasm binary would carry.

NVIDIA has demonstrated one clever use of this: shifting Python execution client-side, into the user's own browser sandbox. That prevents cross-user contamination entirely, and for that particular workload, it removes server-side attack surface altogether, since there's no server executing the code in the first place.

The tradeoffs deserve to be said plainly rather than glossed over. The scientific Python stack, numpy, pandas, scipy, is available through Pyodide's build system, but not every package compiles cleanly. Anything leaning on C-extension dependencies can be missing entirely, or run with noticeably worse performance. Module load time also runs higher than a native Wasm binary, and that matters a lot for orchestration patterns spinning up many short-lived Python tool calls back to back. The overhead adds up fast across a workload making dozens of small Python calls per task. What doesn't change is the isolation guarantee itself: the sandbox boundary holds for Pyodide exactly the way it does for a native Wasm module, which is really the point of building on the same underlying model in the first place.

For teams with heavy Python workloads that genuinely can't move to Pyodide, whether that's a package dependency or a hard performance floor, the next section covers where gVisor or Firecracker pick up the slack. Python running under gVisor gets a meaningfully smaller kernel attack surface without needing to be compiled to Wasm at all.

How production systems layer Wasm with containers and microVMs for defense in depth

Diagram: Three Isolation Layers, Three Different Threats. Visualizes: Show a layered defense-in-depth stack with three named tiers, each mapped to its specific role: (1) Wasm — instruction-level capability gating, best for stateless compute and…

No single isolation technology covers every kind of workload an agent might throw at it, and pretending otherwise is exactly how teams end up with a false sense of security. The pattern showing up again and again across production cloud platforms is Wasm inside containers inside microVMs: three layers, each catching a different failure mode.

Think about what each layer is actually doing. Wasm gives you instruction-level isolation with fine-grained capability gating, which makes it the right fit for stateless compute, plugin execution, and tool-call sandboxing, as long as the language compiles cleanly to Wasm (Rust, C, and Go all do) and the workload doesn't need raw OS access. gVisor intercepts syscalls before they hit the host kernel, which makes it the right choice for arbitrary Linux binaries, Python scripts, Node.js processes, shell commands, anything that can't be compiled to Wasm at all. You eat some performance overhead in exchange for not exposing a shared kernel. Firecracker microVMs sit at the strongest end of the spectrum, giving each tenant a full separate OS with no shared kernel; the right call for long-running, stateful workloads that need a persistent environment to stick around in. AWS Lambda's isolation runs on Firecracker underneath, which tells you something about how much confidence the industry has already staked on it, even though the boot latency rules it out for fast, ephemeral tool calls.

This isn't redundancy for its own sake. Each layer answers a different threat. Wasm catches instruction-level escapes before they happen. The container boundary limits how far a mistake spreads if something does go wrong inside it. The microVM boundary is the backstop keeping the host kernel itself out of reach no matter what. Stack all three, and no single layer has to be perfect on its own.

There's also an efficiency case for Wasm specifically that's easy to undersell. Because the isolation is enforced at the instruction level rather than through a full OS or kernel boundary, thousands of agent instances or MCP servers can get bin-packed into a single Wasm runtime, all sandboxed from each other. That density just isn't achievable with Firecracker, or even with containers, at an equivalent level of per-tenant isolation. So where does that leave things as of this writing? The practitioner consensus heading into 2026 seems to be settling somewhere specific: shared-kernel isolation isn't a defensible default for untrusted agent code anymore. But the fix isn't one technology swapping in to replace another wholesale. It's matching the isolation layer to the workload, and being honest about which layer is actually doing which job.

Wasm and MCP: how the tool-execution boundary is becoming a standard interface

Model Context Protocol has become the standard way agents discover and call tools, and Wasm is increasingly what sits underneath doing the actual execution. That pairing isn't a coincidence. MCP defines how an agent finds and calls a tool; Wasm defines what that tool is allowed to touch once it's running. Put the two together and you get a tool-execution boundary standardized on both ends at once: the interface and the isolation.

Wasmcp is one concrete expression of this: a development kit for building MCP servers as Wasm components, letting a team mix tools written in Rust, Python, TypeScript, and other languages inside a single server binary while keeping each one isolated from the others behind the same capability model. That's a bigger deal than it sounds, because it means the language a tool happens to be written in stops being an infrastructure decision. A team writes the tool in whatever language fits the job, and trusts the isolation boundary around it looks identical no matter which language they picked.

Microsoft's Wassette, mentioned earlier as a Wasmtime-based runtime, points the same direction: deny-by-default permissions, agents pulling components autonomously from OCI registries, all of it wrapped around MCP as the calling convention. What's emerging here isn't just a clever technical stack. It's a standard shape for how agent tool execution gets built from here on out: MCP for the interface, Wasm components for the execution, WASI and the Component Model underneath holding language-agnosticism and capability isolation together at the same time. Whether that shape ends up as durable as the container image format did for an earlier generation of infrastructure is still an open question, and I don't think anyone gets to answer it confidently yet. But the pieces fit together well enough to suggest it might.

Sources

  1. cosmonic.com
  2. thenewstack.io
  3. developer.nvidia.com

More in Code Execution Sandboxes Compared