WASM vs gVisor vs microVM for Untrusted Code
When you decide to run untrusted or AI-generated code, three answers dominate the conversation, and they sit on three genuinely different boundaries. WebAssembly bounds a single program's memory and control flow at the language-runtime level, with a capability set instead of syscalls. gVisor intercepts syscalls in a user-space kernel written in Go, shrinking the host-kernel surface without booting a second OS. A microVM like Firecracker boots a real guest kernel behind CPU hardware virtualization. Most comparisons pit two of these against each other; the real decision is a three-way one, because each covers a different band of the trade between overhead and what it can actually run. This is the fair, honest triangle.
The short version, so you can bail early if you already know your workload: WASM is the pick for tiny, deterministic compute and tightly-scoped plugins where you control the language; gVisor is the pick for container-shaped workloads that want defense-in-depth without full virtualization; a microVM is the pick for arbitrary code that needs a full OS and the hardest multi-tenant boundary. WASM can't hurt the host because it can barely do anything — that's simultaneously the pitch and the limitation. gVisor can do most of what Linux does, unless it's the part it didn't re-implement. A microVM can do all of it, for the price of an OS's worth of memory floor. Everything below is the reasoning, and where I'm stating a workload-specific fact about WASM or gVisor I'll flag that you should verify it against their own docs.
Three boundaries, not one dial
Start with the boundary, because everything else is downstream of it. WASM isolates one program's linear memory and control flow behind a software boundary defined by the capabilities you hand it — the module can only name things you explicitly imported. gVisor isolates a process by putting a user-space kernel between it and the host: the workload's syscalls are intercepted and serviced by the Sentry, which itself talks to the host through a narrow, seccomp-filtered door. A microVM isolates a whole guest operating system behind a hardware boundary enforced by the CPU's virtualization extensions (Intel VT-x, AMD-V, exposed via KVM).
Notice these aren't three points on one "more secure" line. They defend on different axes. WASM is least-privilege by construction — the module can't reach anything it wasn't granted. gVisor is host-kernel-surface reduction — instead of exposing the full 300-plus-syscall Linux ABI to the host, the guest mostly talks to gVisor, and only gVisor talks to the host. A microVM is memory-and-hardware isolation — attacker and victim sit in separate address spaces with a real kernel boundary between them. Reasoning about all three on a single dial is how you end up over-paying for a boundary you didn't need or shipping one that can't run your code.
WASM: capability sandbox, no syscalls by default
A WebAssembly module runs in its own linear memory and structurally cannot address anything outside it — bounds-checked memory, an inaccessible callstack (classic stack-smashing is impossible by construction), and type-checked control transfers. There is no guest kernel and no ambient authority: under WASI's deny-by-default model, the module gets no files, no sockets, no clock even, unless the host hands it a specific capability. As of WASI Preview 2 it can do capability-gated filesystem, sockets, and HTTP — the old "WASM can't network" line was a Preview 1 limitation. Instantiation is sub-millisecond to low-single-digit-milliseconds and per-instance memory lives in the kilobytes-to-low-megabytes range (verify against your runtime — the exact numbers are runtime- and module-dependent).
The limitation is the flip side of the pitch. There's no fork or exec, so no subprocesses. A wasm runtime executes wasm modules, not /usr/bin/ffmpeg or an arbitrary ELF binary. Native-extension code only runs if someone pre-compiled it to wasm — NumPy works in Pyodide because that project ported it, not because WASM runs C extensions generically, so an arbitrary pip install of a native-dep package fails unless a wasm build already exists. You re-implement the world through WASI rather than getting Linux for free. Here's a minimal run with wasmtime, granting exactly one directory and nothing else:
# Compile to the wasm target, then run under wasmtime with a capability set.
# The module sees ONLY /data (mapped to ./sandbox) and nothing else on the
# host: no other files, no network, no subprocesses. That is the whole pitch
# and the whole limitation in one command.
wasmtime run \
--dir ./sandbox::/data \
transform.wasm -- --in /data/input.json
# There is no --dir for the rest of the filesystem, no way to shell out,
# and no ffmpeg unless you shipped ffmpeg-compiled-to-wasm yourself.gVisor: a user-space kernel intercepting syscalls
gVisor (the runtime is runsc) is a user-space kernel written in Go. Its Sentry component re-implements a large fraction of the Linux syscall ABI itself: when the sandboxed workload makes a syscall, gVisor intercepts and services it inside the Sentry rather than passing it straight to the host kernel. The Sentry still touches the host, but through a small, tightly seccomp-filtered set of host syscalls. The payoff is real — you dramatically shrink the host-kernel attack surface without booting a second operating system, and you keep a container-shaped operational model with startup much closer to a container than a VM. It runs untrusted code in production: Google's Cloud Run and GKE Sandbox use it, and per its own docs Modal does too.
The cost lands in two workload-dependent places, and both matter for the decision. First, compatibility: gVisor re-implements the kernel and doesn't implement all of it. Some syscalls and edge-case behaviors are unimplemented or differ subtly, so unusual syscalls, exotic filesystem semantics, or low-level kernel-feature-dependent programs can misbehave or fail. Most ordinary code runs fine; the long tail is where surprises live. Second, performance: because every syscall is intercepted and serviced in user space, syscall- and I/O-heavy workloads pay measurable overhead, while CPU-bound code that rarely calls into the kernel barely notices. Both of these are genuinely workload-specific — verify the specifics against gVisor's own documentation and your own code. Wiring it into Docker looks like a normal container run with a different runtime:
# gVisor installs as a Docker runtime called "runsc". Same container UX,
# but the app's syscalls now hit gVisor's user-space kernel first, not the
# host kernel directly. No guest kernel is booted; this is still a process
# model — it is NOT a hardware VM.
docker run --runtime=runsc --rm python:3.12-slim \
python3 -c 'import statistics; print(statistics.mean([2,4,6,8]))'
# Compatibility caveat: if your workload leans on a syscall gVisor hasn't
# implemented, it can fail here where it would have worked on plain runc.
# Verify your workload against gVisor's docs.microVM: a real guest kernel behind hardware virtualization
A microVM like Firecracker boots a genuine guest kernel isolated by CPU hardware virtualization through KVM. The workload runs against a real Linux kernel of its own — not a re-implementation — and the only path to the host is a minimal virtio device model (virtio-net, virtio-block, virtio-vsock, a serial console) plus the KVM ioctl interface. Firecracker is written in Rust and runs behind a jailer that chroots the process, applies cgroups and seccomp, and drops privileges as defense-in-depth. Because the guest runs a real kernel, compatibility is essentially that of a normal Linux box: if it runs on Linux, it runs in the microVM — native dependencies, unusual syscalls, subprocesses, compilers, package managers, all of it. It's the isolation primitive under AWS Lambda and Fargate, and the strongest of the three multi-tenant boundaries because attacker and victim sit in separate address spaces behind a real kernel.
The price is a higher memory floor — you're paying for an entire OS, not one function or one process — and historically a startup cost. That startup cost is a solved engineering problem, not a law of physics: snapshot-restore makes per-create latency sub-second in practice. The microVM is the heaviest of the three and the only one with no compatibility caveats, because there's a real kernel underneath rather than a capability set or a re-implementation you had to anticipate.
Side by side by side
- Isolation boundary — WASM: software, capability set at the language-runtime level, no guest kernel. gVisor: software, a user-space kernel intercepting syscalls, no guest kernel. microVM: hardware virtualization (KVM) around a real guest kernel.
- Language / syscall support — WASM: only code compiled to wasm, no fork/exec, no arbitrary native binaries; you re-implement host access via WASI. gVisor: broad Linux compatibility but some syscalls unimplemented or subtly different (verify against gVisor's docs). microVM: full Linux — if it runs on Linux, it runs here.
- Cold start — WASM: sub-millisecond to low-single-digit-ms instantiation (verify per runtime). gVisor: container-class, no guest-kernel boot (verify against gVisor's docs). microVM: a guest-kernel boot, but snapshot-restore makes per-create sub-second in practice.
- Memory floor — WASM: kilobytes to low megabytes per instance. gVisor: process-class plus the Sentry's own footprint (verify against gVisor's docs). microVM: an OS's worth — the highest floor of the three.
- Escape blast radius — WASM: attacker and victim share one address space, so the in-process boundary is the more exposed to speculative side channels. gVisor: the Sentry is software that can have bugs; a narrow seccomp-filtered host surface behind it. microVM: separate address spaces and a small, heavily-audited VMM/KVM surface — generally the strongest, though not immune.
- Best-fit workload — WASM: tiny deterministic compute, plugins, per-request expression evaluation. gVisor: container-shaped workloads wanting defense-in-depth without full virt. microVM: arbitrary code, full OS, the hardest multi-tenant boundary.
Which for which workload
Match the boundary to the shape of the workload, and the choice mostly makes itself. Reach for WASM when the code is (or can be) compiled to wasm and you control the language and dependencies — tiny deterministic compute, user-supplied transforms, a rules engine, a math expression, plugins to your own host app, per-request functions you want at a density and cold-start a VM model can't touch. If your workload fits inside a capability set, WASM isn't a compromise, it's the better tool, and a hardware VM would be over-paying for a boundary you can get cheaper.
Reach for gVisor when you want stronger-than-container isolation with a container-shaped operational model, your workloads are well-behaved (no exotic syscalls, not pathologically I/O-bound), and you're comfortable validating compatibility against its implemented surface. It's a legitimate step up from a plain container — defense-in-depth at low operational weight — and for a lot of platform code it's exactly the right amount of boundary.
Reach for a microVM when the code is arbitrary and unpredictable, needs a full OS (a real shell, pip install of anything, subprocesses, compilers, databases), and demands the hardest multi-tenant boundary. This is the AI-agent and general code-execution case exactly: an agent's value is that it runs commands you didn't write and couldn't predict — it shells out, installs native-dep packages, spawns subprocesses. Every one of those is something WASM can't do (or only if pre-ported) and something gVisor might silently fail on if it hits an unimplemented syscall. The honest reason to pick the microVM here isn't that it's "more secure" in the abstract — it's that it's the only one of the three that can run the workload at all, and it happens to come with the strongest boundary. For the full ladder including Kata and confidential VMs, see /blog/code-isolation-hierarchy; for the two-way microVM-vs-gVisor deep dive, see /blog/gvisor-vs-firecracker.
Why PandaStack picks microVMs
PandaStack is built on the microVM boundary 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 capability-scoped wasm module, and not a syscall-intercepting user-space kernel. 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 instead of a surface you had to anticipate. One call gives you a hardware-isolated environment:
from pandastack import Sandbox
# One Firecracker microVM: its own guest kernel under KVM, created via
# snapshot-restore (~179ms p50). Real Linux inside, so "if it runs on
# Linux, it runs here" — no capability set to anticipate, no unimplemented
# syscall to trip over.
with Sandbox.create(
template="code-interpreter", # python + node scientific stack
ttl_seconds=300, # reaped automatically if abandoned
metadata={"task": "agent-tool-call"},
) as sb:
# pip install of a native-dep package — the exact thing WASM can't do
# generically and gVisor might not implement — just works.
sb.exec("pip install numpy -q")
out = sb.exec("python3 -c 'import numpy as np; print(np.mean([2,4,6,8]))'")
print(out.stdout, out.exit_code)
# Context manager destroys the microVM here — nothing survives to next task.The classic objection to the VM boundary 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 ~179ms p50 (about 203ms p99), and only the very first cold boot of a template takes around 3s. A same-host copy-on-write fork — guest memory plus an XFS-reflink rootfs — lands in roughly 400–750ms, a cross-host fork in 1.2–3.5s, and a managed Postgres database (its own durable-volume microVM) in 30–90s. Per-sandbox network egress isolation runs across 16,384 pre-allocated /30 subnets per agent. 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 behind /dev/kvm you control. None of that makes the microVM the right answer for a tightly-scoped wasm plugin; if your workload fits WASM, use WASM, and if it fits gVisor's compatibility surface, gVisor is a fine step up. 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
What's the difference between WASM, gVisor, and a microVM for untrusted code?
They isolate on three different boundaries. WebAssembly bounds one program's memory and control flow at the language-runtime level behind a capability set — no syscalls by default, no guest kernel. gVisor is a user-space kernel (the Sentry, written in Go) that intercepts and re-implements syscalls, shrinking the host-kernel attack surface without booting a second OS. A microVM like Firecracker boots a real guest kernel isolated by CPU hardware virtualization (KVM). WASM is lightest and least capable; gVisor is container-shaped with broad-but-imperfect compatibility; a microVM is heaviest, fully Linux-compatible, and the strongest multi-tenant boundary.
Which is best for running arbitrary AI-generated code?
A microVM. An agent decides at runtime what to install and run — it shells out, spawns subprocesses, and pip-installs native-dependency packages. WASM can't do those things (no fork/exec, no arbitrary native binaries, native extensions only if pre-compiled to wasm), and gVisor can silently fail if the code hits a syscall it hasn't implemented. A microVM runs a real Linux kernel, so if it runs on Linux it runs in the sandbox, and it gives you the strongest boundary — separate address spaces behind a small, audited VMM/KVM surface. WASM and gVisor remain great for their own workloads; verify their compatibility against their own docs.
Is gVisor a virtual machine?
No. gVisor keeps a process model and re-implements Linux syscalls in user space; it does not boot a guest kernel. It can run on a KVM platform for faster, safer address-space switching, but even then it borrows CPU virtualization extensions rather than running a hardware VM. A microVM like Firecracker, by contrast, boots a genuine guest kernel under KVM. Calling gVisor a VM is a common and consequential mistake in security reviews.
How much lighter is WASM than a microVM?
Considerably lighter 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 (verify against your specific runtime and module). A microVM carries an entire OS's memory floor and a guest-kernel boot, though snapshot-restore makes per-create latency sub-second in practice — PandaStack restores a baked Firecracker snapshot at about 179ms p50. You trade that startup and density for the ability to run an entire OS with no compatibility caveats.
Can I self-host a microVM-based sandbox platform?
Yes. PandaStack's 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 as Firecracker microVMs on infrastructure you control, behind a /dev/kvm you own. Every create restores a baked snapshot at ~179ms p50 (about 203ms p99), a same-host copy-on-write fork lands in roughly 400–750ms, and per-sandbox egress isolation runs across 16,384 pre-allocated /30 subnets per agent. A hosted offering exists too, but self-host is first-class.
49ms p50 cold start. Fork, snapshot, and scale to zero.