WebAssembly vs MicroVMs for Sandboxed Code
WebAssembly and microVMs both get sold as "the safe way to run untrusted code," and both deliver — but they isolate completely different things behind completely different boundaries. WASM bounds a single program's memory and control flow at the language-runtime level. A microVM (Firecracker) boots a whole guest OS behind a hardware-virtualization boundary enforced by the CPU. Pick the wrong one and you either ship a boundary that can't run your workload, or pay for an OS where a sandboxed function would have done. This post is the honest comparison: what each actually isolates, what WASM genuinely cannot run, how the overhead differs, and the rule for choosing.
The short version, so you can stop reading early if you need to: they're largely complementary, not strictly competing. WASM trades capability for density and millisecond startup; a microVM trades some density and startup for the ability to run anything Linux can run, behind a hardware boundary. If you control the language and dependencies, WASM is a beautiful fit. If you're running arbitrary, model-generated code with native dependencies and subprocesses, you're on the microVM rung — which is the one PandaStack is built on. Everything below is the reasoning.
What each actually isolates
Start with the boundary, because everything else follows from it. A microVM isolates a whole operating system behind a hardware boundary; WASM isolates a single program's memory and control flow behind a software boundary defined by a capability set. That one sentence is the entire comparison — the rest is detail.
Firecracker uses hardware-virtualization isolation through KVM. The guest runs a real Linux kernel inside a VM, and the boundary is enforced by the CPU's virtualization extensions (Intel VT-x/EPT, AMD-V/NPT). Firecracker's threat model treats every guest vCPU thread as malicious from the moment it starts, and contains it with a layered defense: KVM hardware isolation, a deliberately minimal device model (virtio-net, virtio-block, virtio-vsock and a serial console — almost nothing else to attack), a jailer that chroots the process and drops privileges before exec'ing Firecracker as an unprivileged user, and a default-on seccomp filter that allows only the minimal host syscall set. The boundary is guest ↔ VMM ↔ host.
WASM/WASI — Wasmtime is the common runtime — does software fault isolation at the language and runtime level. There is no guest kernel. The sandbox comes from WebAssembly's design itself: linear memory is bounds-checked, the callstack is structurally inaccessible to the module (classic stack-smashing on return addresses is impossible by construction), and every control transfer goes to a type-checked destination. The module reaches the outside world only through explicitly imported functions — a capability-based model where it gets a specific pre-opened directory or socket, or nothing at all. Wasmtime layers defense-in-depth on top: guard regions before linear memory, stack guard pages, and memory zeroing between instances.
Threat-model honesty: neither is magic
This is the part that gets blog posts in trouble, so I'll be precise. Firecracker's boundary is hardware-enforced and is the industry standard for running mutually-untrusting tenant code; the defense-in-depth layers exist precisely so that a single bug doesn't equal an escape. WASM's boundary is software-enforced and excellent for fine-grained least-privilege, where the module simply can't name anything you didn't grant it.
Both are exposed to CPU microarchitectural and Spectre-class transient-execution attacks. This is the critical symmetry, and the asymmetry inside it. Wasmtime's own security documentation describes Spectre mitigation as a subject of ongoing research and only partial — for example, on aarch64 the relevant speculation barrier has been disabled by default for performance in the version documented at the time of writing (a moving target across releases). That matters because in WASM the attacker and victim share one address space, which makes the in-process boundary the more exposed of the two to speculative side channels. A hardware VM boundary is generally considered stronger against this class because attacker and victim sit in separate address spaces — but stronger is not immune. No general-purpose system fully eliminates microarchitectural side channels, and KVM itself has had real guest-to-host escape CVEs (Google's kvmCTF pays up to $250,000 for one). State the difference as "stronger," never as "safe."
One more operator-facing detail people miss: Firecracker does no network filtering. It explicitly treats all egress traffic from a guest as untrusted and leaves network policy to you. The microVM gives you the isolation primitive; egress control is your job, not the VMM's. PandaStack handles that layer with NATID — every sandbox gets its own Linux network namespace, veth pair, and tap device, with per-sandbox egress isolation across 16,384 pre-allocated /30 subnets per agent.
What WASM can and cannot run
This is where the choice usually gets made in practice, because it's binary: either your workload compiles to the wasm target and lives within a capability set, or it doesn't. WASM runs code compiled to wasm — Rust, C/C++, Go (TinyGo or native Go wasm), AssemblyScript, and CPython via Pyodide. And it does more than the stale lore suggests: as of WASI Preview 2 (stabilized in late 2024 and mainstream through 2025–2026), modules can do filesystem access and TCP/UDP sockets and HTTP through capability-gated host interfaces. The old "WASM can't do networking" line was a Preview 1 limitation and is no longer true. So far, so capable.
Here is what is still true in 2026 — the hard limits that decide the question for arbitrary code:
- No fork / exec. WASI does not implement these POSIX process-management calls. You cannot spawn arbitrary subprocesses or shell out from inside a wasm module.
- No arbitrary native Linux binaries. A wasm runtime executes wasm modules, not /usr/bin/ffmpeg, gcc, apt, or psql. Anything not compiled to wasm doesn't run, barring heavyweight emulation like container2wasm, which is a research-grade workaround rather than a default path.
- Native extension code only if pre-compiled to wasm. NumPy, pandas, and SciPy work in Pyodide because the Pyodide project specifically ported those C/Fortran extensions to wasm — not because WASM runs them generically. An arbitrary pip install of a package with a C extension fails unless a wasm build already exists.
- Limited threads and multiprocessing. Pyodide today is single-threaded with no multiprocessing. General wasi-threads is not the stable Preview 2 story; full threads and native async are the headline of Preview 3, still in development (expected roughly 2026–2027).
Be precise about the Python case, because it's the one people most often get wrong in both directions. The accurate claim is not "NumPy doesn't work in WASM" — it does, in Pyodide, because someone ported it. The accurate claim is that an arbitrary native-extension Python package won't work unless a wasm build exists for it. For a code interpreter where users run whatever pip installs, that distinction is the whole ballgame: the long tail of native-dep packages is exactly what breaks, and you can't predict in advance which one a user (or an agent) will reach for.
The overhead character: where WASM genuinely wins
Credit where it's due — for the workloads it fits, WASM is dramatically lighter, and pretending otherwise would be dishonest. WASM instantiation is sub-millisecond to low-single-digit-milliseconds (no kernel, no VM to boot), with per-instance memory in the kilobytes-to-low-megabytes range. That is genuinely one to two orders of magnitude lighter than a microVM for the right workload, which is why edge platforms run untrusted per-request functions as wasm at a density a VM model can't touch. The honest caveats: AOT-compiled execution is fast but near-native, not native — there's a real, workload-dependent runtime overhead versus bare metal — and if a module isn't AOT-compiled or cached, compilation has its own cost. I'm keeping these as ranges on purpose; the exact instantiation number is runtime- and module-dependent, and anyone quoting a single canonical figure is rounding off reality.
Firecracker's own published figures are also impressive for what they are: user-space or application code in as little as 125 ms, under 5 MiB of memory overhead per microVM, and up to 150 microVMs per second per host. Those are Firecracker's numbers for a bare kernel boot — and they are deliberately separate from PandaStack's create latency, which measures a different and heavier thing. PandaStack's p50 create is 179 ms (about 203 ms p99), but that's a snapshot-restore of a fully-booted machine, not a cold kernel boot: there's no warm pool of idle VMs, and every create restores a baked Firecracker snapshot on demand. Don't conflate the two metrics — Firecracker's 125 ms is a boot-to-userspace figure; PandaStack's 179 ms is a restore-the-whole-machine figure. Both are far heavier than a wasm instantiation, and that's the trade you're buying: an entire OS instead of one function.
Use WASM when…
Reach for WASM/WASI when the shape of the problem matches its strengths:
- The code is, or can be, compiled to wasm — and you control or constrain the language and dependencies.
- You want massive density and millisecond cold-starts: edge functions, plugin systems, per-request isolation, untrusted expressions or scripts evaluated at scale.
- You don't need subprocesses, arbitrary native binaries, or a full POSIX environment — the workload lives entirely within a capability set you define.
- The untrusted surface is something you can tightly scope: a user-supplied transform, a rules engine, a math expression, an extension to your own host application.
If that's your situation, WASM is not a compromise — it's the better tool. A hardware VM would be over-paying for a boundary you can get cheaper, and you'd lose the density and startup that made the workload economical in the first place. The isolation hierarchy post frames where WASM sits relative to the kernel-to-VM ladder: it's off to one side, isolating on a different axis, not a higher rung of the same one. See /blog/code-isolation-hierarchy for that map.
Use a microVM when…
Reach for a microVM (Firecracker) when the workload needs the OS, the boundary, or both:
- You must run arbitrary, unmodified code and native binaries: a real shell, pip install of anything, compilers, databases, multiple processes, package managers, a full filesystem, an actual kernel.
- You're running mutually-untrusting multi-tenant workloads and want a hardware isolation boundary rather than a shared in-process one.
- You can't predict the dependencies in advance — the defining property of AI-generated code, where the agent decides at runtime what to install and execute.
- You need to snapshot and fork whole-machine state — freeze an environment mid-task and branch it — which is a VM-level capability, not a function-level one.
This is exactly the AI-agent and general code-execution case. An agent's value is that it runs commands you didn't write and couldn't fully predict: it shells out, installs native-dep packages, spawns subprocesses, edits files, hits the network. Every one of those is something WASM either can't do or can only do if the dependency was pre-ported. The honest reason to pick the microVM rung here isn't that it's "more secure" in the abstract — it's that it's the only rung that can run the workload at all, and it happens to come with a hardware boundary. For the deeper walk through why the container rung underneath it isn't enough for hostile code, see /blog/what-is-a-microvm.
Where PandaStack lands
PandaStack is built on the microVM rung because that's where arbitrary, untrusted, model-generated code belongs. Every sandbox is a Firecracker microVM with its own guest kernel (5.10, Ubuntu 24.04 guest), isolated by KVM hardware virtualization — not a shared-kernel container, and not an in-process wasm sandbox. That means pip install of anything, a real shell, subprocesses, compilers, and the full long tail of native-dependency Python all just work, because there's a real Linux kernel underneath rather than a capability set you had to anticipate.
The classic objection to the VM rung is cost, and that's the part the engineering answers. There's no warm pool of idle VMs; every create restores a baked snapshot on demand at a 179 ms p50 (about 203 ms p99), and a same-host fork — copy-on-write guest memory plus an XFS-reflink rootfs — lands around 400 ms, so you can branch a running environment cheaply. The core is Apache-2.0 and self-hostable on your own Linux KVM hosts: you run the control-plane API and a per-host agent, and sandboxes execute on your infrastructure, behind /dev/kvm you control. A hosted offering exists too, but self-host is first-class. None of that makes the microVM the right answer for a tightly-scoped plugin — if your workload fits WASM, use WASM. It makes the microVM the right answer for the workload it's actually for: arbitrary code you didn't write and can't fully predict.
Frequently asked questions
Is WebAssembly or a microVM better for sandboxing untrusted code?
Neither is universally better — they isolate different things. WASM bounds a single program's memory and control flow at the language-runtime level behind a capability set, which is ideal for fine-grained plugins and code you can compile to wasm. A microVM (Firecracker) isolates a whole guest OS behind a hardware-virtualization boundary (KVM), which is what you need to run arbitrary native binaries, subprocesses, and native-dependency code. For arbitrary AI-generated code, the microVM rung is usually the only one that can run the workload at all.
Can WebAssembly run arbitrary Python or native Linux binaries?
No. A wasm runtime executes wasm modules, not native Linux ELF binaries like ffmpeg, gcc, or psql, and WASI does not implement fork or exec, so you can't spawn subprocesses. Python runs via Pyodide, and packages like NumPy and pandas work only because they were specifically ported to wasm — an arbitrary pip install of a package with a C extension fails unless a wasm build already exists. For unconstrained native code you need a real OS, which is the microVM case.
Is a microVM immune to side-channel attacks and WASM isn't?
No. Both are exposed to CPU microarchitectural and Spectre-class transient-execution attacks. The difference is that in WASM the attacker and victim share one address space, making the in-process boundary the more exposed of the two, while a hardware VM puts them in separate address spaces, which is generally considered stronger against this class. But no general-purpose system fully eliminates microarchitectural side channels, and KVM has had real guest-to-host escape CVEs — so frame it as stronger, never as immune.
How much faster does WASM start than a microVM?
Considerably faster for the workloads it fits — WASM instantiation is sub-millisecond to low-single-digit-milliseconds with per-instance memory in the kilobytes-to-low-megabytes range, one to two orders of magnitude lighter than a VM. Firecracker reports user-space code in as little as 125 ms for a cold kernel boot; PandaStack's snapshot-restore create is 179 ms p50, a different and heavier metric because it restores a fully-booted machine rather than a single function. You trade that startup and density for the ability to run an entire OS.
49ms p50 cold start. Fork, snapshot, and scale to zero.