WebAssembly vs Firecracker for Untrusted Code
If you're choosing how to run code you don't trust, WebAssembly and Firecracker both belong on the shortlist — and they solve the problem from opposite ends. WASM starts from a program that can do nothing and hands it capabilities one by one; the module can only touch what you explicitly imported. Firecracker starts from a full Linux machine that can do everything, and wraps a hardware wall around it so that "everything" stays inside one guest. WASM asks "may I?" before every syscall; a container just does it and hopes; a microVM lets the guest do it and contains the blast radius. This post is the untrusted-code comparison specifically: what each isolation model actually guarantees, what WASM genuinely cannot run, and the honest rule for picking one.
The one-line verdict, up front: WASM wins when you control compilation to a constrained target and want maximum density plus microsecond-class startup. Firecracker (and platforms built on it, like PandaStack) wins when you must run arbitrary, unmodified, full-OS workloads — the kind an LLM generates when it decides at runtime to `pip install` something with a native extension. Everything below is why.
Two isolation models, one question
WASM's model is capability-based software isolation. A WebAssembly module runs in a sandbox defined by the language and runtime, not by a kernel. Its linear memory is a single bounds-checked region — every load and store is range-checked, so a module physically cannot address memory outside its own heap. The callstack is structurally inaccessible (return addresses aren't values the module can forge), and every control transfer lands on a type-checked target. Critically, there are no ambient syscalls: the module reaches the outside world only through host functions you import. WASI is the standardized version of that host interface — a capability-gated set of imports for files, sockets, clocks, and the like. Hand a module a single pre-opened directory and that's all the filesystem it will ever see. Hand it nothing and it's pure compute in a box.
Firecracker's model is hardware isolation. Each microVM boots a real Linux kernel and is confined by KVM — the CPU's virtualization extensions enforce the boundary, not a syscall filter in software. The guest talks to the host through a deliberately tiny device model (virtio-net, virtio-block, virtio-vsock, a serial console — almost nothing else to attack), and Firecracker layers a jailer (chroot + dropped privileges) and a default-on seccomp filter around the VMM process. The guest can run any Linux binary in any language, unmodified, because there's a genuine OS underneath. The cost is a larger memory footprint and millisecond-not-microsecond startup: you're booting or restoring a machine, not instantiating a function.
Side by side
- Isolation boundary — WASM: software fault isolation + capability set at the runtime level (no guest kernel). Firecracker: hardware virtualization (KVM) with a full guest kernel.
- What it runs — WASM: only code compiled to the wasm target, within imported capabilities. Firecracker: any unmodified Linux binary, any language, plus subprocesses, package managers, and compilers.
- Startup — WASM: fast, microsecond-to-millisecond-class instantiation (verify against your runtime's docs). Firecracker: milliseconds — ~3s first cold boot, or ~49ms when restoring a snapshot.
- Memory footprint — WASM: kilobytes to low megabytes per instance. Firecracker: a few MB of VMM overhead plus the guest's baked RAM (whole-OS footprint).
- Syscalls / subprocesses — WASM: no ambient syscalls; no fork/exec, so no arbitrary subprocesses. Firecracker: full POSIX — fork, exec, threads, shells, everything.
- Attack surface you own — WASM: the host runtime's compiler/JIT and the capabilities you grant. Firecracker: the VMM device model + hypervisor (small, heavily audited).
- Best fit — WASM: code you compile and constrain, at high density. Firecracker: arbitrary, unpredictable, full-OS workloads (e.g. AI-generated code).
What WASM can't run (the part that decides it)
For untrusted code the choice usually comes down to a binary question: does your workload compile to wasm and live within a capability set, or not? WASM is more capable than the stale lore suggests — as of WASI Preview 2, modules can do capability-gated filesystem access, TCP/UDP sockets, and HTTP, so "WASM can't do networking" is a retired Preview 1 complaint. But three hard limits still decide the arbitrary-code case in 2026:
- No fork/exec. WASI doesn't implement POSIX process management. A module can't spawn a subprocess or shell out — which rules out anything that runs another program (a build step, a linter, `git`, a headless browser).
- No arbitrary native binaries. A wasm runtime executes wasm modules, not `/usr/bin/ffmpeg`, `gcc`, `apt`, or `psql`. If it wasn't compiled to wasm, it doesn't run — barring research-grade emulation like container2wasm, which isn't a default path.
- Native extensions only if pre-ported. NumPy and pandas work in Pyodide because the project specifically compiled those C/Fortran extensions to wasm. An arbitrary `pip install` of a package with a native extension fails unless a wasm build already exists — and you can't predict which package a user or an agent will reach for.
There's also a subtler cost people forget: with WASM, the host runtime is now part of your trusted computing base. The whole boundary rests on the runtime's compiler/JIT correctly enforcing bounds checks and control-flow integrity in software. That's a real, actively-maintained surface — and because the untrusted module and the runtime share one address space, a speculative side-channel (Spectre-class) attack has attacker and victim in the same process. Wasmtime's own docs treat Spectre mitigation as partial and ongoing. A hardware VM boundary is generally considered stronger against that class because attacker and victim sit in separate address spaces — stronger, not immune. KVM has had real guest-to-host escape CVEs too. Neither is magic; state the difference as "stronger," never "safe."
Where WASM genuinely wins
Credit where it's due: for the workloads it fits, WASM is dramatically lighter and faster to start than any VM, and pretending otherwise would be dishonest. Instantiation is microsecond-to-millisecond-class with per-instance memory in the kilobytes-to-low-megabytes range — one to two orders of magnitude lighter than a microVM. (Exact numbers are runtime- and module-dependent; check your runtime's docs rather than trusting a single canonical figure.) That's why edge platforms run untrusted per-request functions as wasm at densities a VM model can't touch. Reach for WASM when:
- You control the source — the code is, or can be, compiled to wasm, and you constrain the language and dependencies.
- You want extreme density and near-instant cold starts: edge functions, plugin systems, per-request isolation, untrusted expressions or user-supplied transforms evaluated at scale.
- The workload lives entirely inside a capability set — no subprocesses, no arbitrary native binaries, no full POSIX environment needed.
- The untrusted surface is something you can tightly scope: a rules engine, a math expression, a scripting extension to your own host application.
In that situation 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 while losing the density that made the workload economical. A minimal WASI run looks like this — a single module, granted exactly one directory:
# Compile Rust to the WASI target, then run it under wasmtime.
# The module gets ONE pre-opened dir (--dir) and nothing else —
# no ambient filesystem, no network, no subprocesses.
cargo build --target wasm32-wasip1 --release
wasmtime run --dir=./data::/data target/wasm32-wasip1/release/transform.wasm
# Try to shell out or read /etc/passwd from inside that module?
# There's no syscall to do it — the capability was never imported.Where Firecracker wins
Reach for a microVM the moment the workload needs the OS, the boundary, or the unpredictability handled:
- 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 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 model decides at runtime what to install and execute.
- You need whole-machine snapshot and fork — 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, and it's worth being blunt about why. An agent's entire value is that it runs commands you didn't write and couldn't fully anticipate: it shells out, installs native-dependency packages, spawns subprocesses, compiles things, 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 Firecracker here isn't that it's "more secure" in the abstract — it's that it's often the only model that can run the workload at all, and it happens to arrive with a hardware boundary attached.
Where PandaStack lands
PandaStack runs on Firecracker because that's where arbitrary, untrusted, model-generated code belongs. Every sandbox is a Firecracker microVM with its own guest kernel (Ubuntu 24.04 guest), isolated by KVM — not a shared-kernel container, and not an in-process wasm sandbox. So a `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 genuine Linux kernel underneath rather than a capability set you had to anticipate:
from pandastack import Sandbox
sbx = Sandbox.create(template="code-interpreter")
# Arbitrary, unmodified native code — no wasm build required.
# In WASM this would fail the moment a native extension appears.
sbx.exec("pip install duckdb pandas")
out = sbx.exec("python -c \"import duckdb; print(duckdb.sql('select 42').fetchone())\"")
print(out.stdout) # (42,)
# Shell out, spawn subprocesses, compile — all fine on a real kernel.
sbx.exec("gcc --version && git init /tmp/repo")The classic objection to the VM model 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 — ~49ms in the fast path, 179ms p50, about 203ms p99, versus ~3s only on the first cold boot before a snapshot exists. Same-host forks branch a running environment via copy-on-write memory plus an XFS-reflink rootfs. The core is open-source and self-hostable on your own Linux KVM hosts. None of that makes Firecracker the right answer for a tightly-scoped plugin — if your workload fits WASM, use WASM. It makes Firecracker 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 Firecracker better for running untrusted code?
Neither is universally better — they isolate different things. WASM confines one program's memory and control flow behind a capability set at the runtime level, which is ideal when you control compilation and want maximum density and microsecond-class startup. Firecracker isolates a whole Linux guest behind a hardware boundary (KVM), which is what you need to run arbitrary, unmodified native binaries, subprocesses, and native-dependency code. For arbitrary AI-generated code, Firecracker is usually the only model that can run the workload at all.
Can WebAssembly run any language or native Linux binary?
No. A wasm runtime executes only code compiled to the wasm target, not native Linux ELF binaries like ffmpeg, gcc, or psql, and WASI does not implement fork or exec, so you cannot spawn subprocesses. Python runs via Pyodide, and packages like NumPy work only because they were specifically ported to wasm — an arbitrary pip install of a package with a native extension fails unless a wasm build already exists. For unconstrained native code you need a real OS, which is the Firecracker case.
Does WASM start faster than a Firecracker microVM?
Yes, considerably, for the workloads it fits. WASM instantiation is microsecond-to-millisecond-class with per-instance memory in the kilobytes-to-low-megabytes range — one to two orders of magnitude lighter than a VM (verify exact numbers against your runtime's docs). Firecracker is millisecond-class: a first cold boot is around 3 seconds, but restoring a baked snapshot lands near 49ms in the fast path. You trade WASM's startup and density for the ability to run an entire operating system unmodified.
Is Firecracker 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 untrusted module and the host runtime share one address space, so attacker and victim sit in the same process, whereas 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 immune.
What does PandaStack use to run untrusted code?
PandaStack runs every sandbox as a Firecracker microVM with its own guest kernel, isolated by KVM hardware virtualization. That lets untrusted, model-generated code run arbitrary unmodified binaries — pip install of anything, real shells, subprocesses, compilers, native-dependency Python — behind a hardware boundary. To keep VM-grade isolation cheap, there's no warm pool: each create restores a baked snapshot on demand (~49ms fast path, 179ms p50), and the core is open-source and self-hostable on your own Linux KVM hosts.
49ms p50 cold start. Fork, snapshot, and scale to zero.