all posts

Firecracker vs WebContainers: where should untrusted code run?

Ajay Kumar··9 min read

There are two genuinely different answers to "where do I run code I didn't write?" and only one of them involves a server. StackBlitz's WebContainers takes the radical option: don't run the code anywhere, run it in the user's own browser tab, on a Node-compatible runtime compiled to WebAssembly. Firecracker takes the conventional-but-hardened option: run it on your machines, but give every workload its own guest kernel behind hardware virtualization. Both are legitimate answers to the untrusted-code problem. They are so structurally different that comparing them mostly means asking better questions about your workload.

I built PandaStack on Firecracker, so my bias is on the record. That said, WebContainers is one of the most impressive pieces of engineering shipped in the browser in the last decade, and for a large class of products it is simply the right call — including some products that are currently paying for servers they don't need. What follows is an honest map of where each one lives.

What WebContainers actually is

WebContainers is a runtime that reimplements enough of an operating system, in WebAssembly, that Node.js programs believe they're on a machine. It runs inside a browser tab. There is no server, no container, and no VM anywhere in the picture. The three pieces that make it feel real are a virtual filesystem (an in-memory tree the WASM runtime serves as if it were a disk), a Node-compatible runtime and process model (so `npm install` and `node server.js` do something sensible), and a networking shim where a "server" that binds a port is intercepted and routed by a service worker, so a preview iframe can fetch from it over a synthetic origin.

The sleight of hand worth appreciating: it is not emulating x86 Linux. There's no ELF loader, no real syscall table, no kernel underneath. It's a from-scratch reimplementation of the parts of an OS that Node programs actually touch — `fs`, `child_process`, a socket-shaped API, a package manager that happens to be written in JavaScript anyway — mapped onto browser primitives. That's why it starts instantly and why it can't run your Postgres binary. Those are the same fact.

Verify any specific capability against StackBlitz's own docs before you design around it. WebContainers is under active development, the supported surface has grown steadily, and a limitation that was true a year ago may not be true today. Everything qualitative here is about the model, not a snapshot of the feature matrix.

The properties that make it genuinely great

It's easy to read "runs in the browser" as a compromise. It isn't. The model has four properties that a server-side sandbox can never have, at any price.

  • Zero marginal compute cost. Ten users or ten million, your bill is the CDN cost of shipping a bundle. Every user brings their own CPU. For a docs playground or a framework demo, this converts an infrastructure line item into a static asset.
  • Instant startup with nothing to schedule. There's no VM to place, no image to pull, no queue, no cold-start tail. The runtime boots in the tab that's already open.
  • The code never leaves the user's machine. For anything touching proprietary source or regulated data, "we technically cannot see it" is a much stronger statement than "we promise not to look." It also makes the compliance conversation dramatically shorter.
  • Per-user isolation for free, from the most attacked sandbox on earth. Each user runs in their own browser's sandbox — a boundary that has absorbed two decades of adversarial attention and a small nation's worth of bug bounty money. And a user attacking that sandbox is attacking their own laptop, which sharply limits their enthusiasm.

That last point deserves emphasis because it's the one people misjudge. In a server-side sandbox, isolation is a cost you engineer and pay for. In WebContainers, the blast radius of malicious code is the machine of the person who ran it. The threat model mostly evaporates.

The hard limits, which are structural

None of the following are bugs or roadmap items. They fall directly out of "there is no kernel here."

It is not real Linux

You cannot run an arbitrary compiled binary, because nothing in the tab knows how to load and execute one. That rules out Docker, Postgres, Redis, ffmpeg, ImageMagick, a system compiler toolchain, and any language runtime that hasn't been explicitly ported. `apt-get install` is not a concept. Native npm dependencies that compile at install time — the node-gyp family, image and database bindings, browser automation drivers — are where in-browser execution most often runs out of road, and it's exactly where a user-supplied `package.json` likes to wander. Check the current support surface against StackBlitz's docs; the boundary moves, but the shape of it doesn't.

Networking is a shim, not a stack

There are no raw sockets in a browser tab. Outbound requests are browser `fetch` calls, which means CORS applies and anything else needs a proxy you host — quietly reintroducing a server to the architecture. A dev server inside the container never binds a port that anything outside the browser can reach; the preview URL is a service-worker illusion, which is elegant for showing a user their own app and useless for exposing an endpoint to a webhook, a teammate, or another service.

One tab, one machine, one user's patience

Memory is bounded by what the tab can allocate, and CPU is bounded by the laptop it's running on — a laptop that is also on a video call. A heavy install or a long build competes with the user's actual work, and on a mid-range machine it will lose. Close the tab and the process, filesystem, and running server all cease to exist. That's a clean lifecycle if the workload is a session; it's a data-loss event if the workload is a job.

It cannot run when nobody is looking

This is the limit that matters most for AI, and it's the one least discussed. WebContainers needs a browser. An autonomous agent grinding through a task queue at 3am has no tab open, no user, and no screen. You can technically drive headless Chrome on a server to host it — at which point you're operating a browser to avoid operating a VM, and you've reacquired all the server costs without acquiring a kernel. That trade rarely survives contact with a spreadsheet.

// Runs entirely in the USER'S browser tab. Your servers see none of this.
import { WebContainer } from '@webcontainer/api';

// Requires cross-origin isolation (COOP/COEP headers) -- the runtime needs
// SharedArrayBuffer, so it only works on pages you serve with those set.
const wc = await WebContainer.boot();

// The "filesystem" is a JS object graph living in tab memory. No disk.
await wc.mount({
  'package.json': {
    file: {
      contents: JSON.stringify({
        name: 'playground',
        type: 'module',
        scripts: { dev: 'vite' },
        devDependencies: { vite: '^5.0.0' },
      }),
    },
  },
  'index.html': { file: { contents: '<h1>hello from a browser tab</h1>' } },
});

// npm is JavaScript, so it runs. A dependency with a node-gyp build step
// is where this pipeline tends to discover it is not on Linux.
const install = await wc.spawn('npm', ['install']);
install.output.pipeTo(new WritableStream({ write: (c) => console.log(c) }));
if ((await install.exit) !== 0) throw new Error('install failed');

await wc.spawn('npm', ['run', 'dev']);

// Nothing binds a real TCP port. A service worker intercepts requests to a
// synthetic origin and routes them to the in-tab "server".
wc.on('server-ready', (port, url) => {
  document.querySelector('iframe').src = url;
});

// Close the tab and the process, the filesystem, and the server all vanish.
// Nothing to bill, nothing to reap, nothing to page you about at 2am.

What a Firecracker microVM gives you instead

A Firecracker microVM is the opposite bet: instead of reimplementing an OS small enough to fit in a tab, you run a real one and make it cheap. Each sandbox gets its own Linux guest kernel, confined by KVM hardware virtualization — the model AWS Lambda uses to run untrusted code from millions of customers. Inside it, everything is boring in the best sense: real syscalls, arbitrary ELF binaries, any language runtime you can install, a package manager that works, a local Postgres if you want one, a real network namespace with a tap device and an actual routable address.

The historical objection was that VMs are too slow and too heavy to spin up per task. Snapshot-restore is what retires that objection. On PandaStack there is no warm pool of idle VMs; every create restores a baked snapshot on demand, at p50 179ms and p99 around 203ms, of which the restore step itself is roughly 49ms. Only the first-ever spawn of a template pays a cold boot of about 3 seconds. Copy-on-write memory (`MAP_PRIVATE`) and a reflinked rootfs mean a hundred sandboxes from the same template share pages until they diverge, and userfaultfd streaming pages guest memory in from object storage on demand rather than downloading it first.

You also get primitives a browser tab structurally cannot offer. `snapshot()` freezes a machine mid-execution, including its memory. `fork()` clones a running sandbox — 400–750ms on the same host, 1.2–3.5s across hosts — so an agent can branch three approaches from one warm state instead of rebuilding it three times. Sandboxes can be persistent with durable volumes for stateful work, hibernate to zero when idle, and wake later. A managed Postgres is a create call away, at 30–90s because a real database really does have to bootstrap. And capacity isn't a toy: each agent pre-allocates 16,384 /30 subnets for per-sandbox networking.

The bill for all of this is honest and unavoidable: those are servers. You pay for them whether or not anyone is using them, you patch them, you monitor them, and someone owns the pager. WebContainers' users pay for their own compute; yours don't.

from pandastack import Sandbox

PKG = b'{"name":"playground","type":"module","scripts":{"build":"vite build"},"devDependencies":{"vite":"^5.0.0"}}'
HTML = b'<h1>hello from a real guest kernel</h1>'

# One Firecracker microVM, restored from a baked snapshot in ~179ms (p50).
# No browser involved -- this runs fine from a cron job at 3am.
with Sandbox.create(template="base", ttl_seconds=900,
                    metadata={"job": "build-user-project"}) as sbx:
    # 1. Push project files into the guest's real filesystem.
    sbx.filesystem.write("/work/package.json", PKG)
    sbx.filesystem.write("/work/index.html", HTML)

    # 2. Install. Real npm on a real kernel, so native postinstall steps
    #    (node-gyp, sharp, better-sqlite3) actually compile instead of
    #    explaining politely that they cannot.
    install = sbx.exec("cd /work && npm install", timeout_seconds=600)
    assert install.exit_code == 0, install.stderr

    # 3. Build, then read the output back out.
    build = sbx.exec("cd /work && npm run build", timeout_seconds=900)
    print(build.stdout)
    print(sbx.exec("ls -la /work/dist", timeout_seconds=30).stdout)

    # 4. Real Linux means the boring things work: other languages, system
    #    packages, compilers, /proc, raw sockets, a local database.
    probe = sbx.exec("uname -r && python3 -c 'import sqlite3; print(\"ok\")'",
                     timeout_seconds=30)
    print(probe.stdout, probe.exit_code)

    # 5. Freeze the warm project so the next run skips node_modules, and
    #    fork() from it when an agent wants to try three fixes at once.
    snap = sbx.snapshot()
    print("snapshot:", snap)

# VM destroyed at block exit. Nobody had a tab open the entire time.

Side by side

Nine dimensions that actually change your architecture. WebContainers' capabilities evolve quickly — verify anything load-bearing against StackBlitz's own docs rather than this table.

  • Isolation model — WebContainers: the browser's own sandbox, per user, per tab; the attacker and the victim are the same person. Firecracker: hardware-virtualized microVM with its own guest kernel, per workload, on your host.
  • Where compute runs — WebContainers: on the end user's device, in a tab they opened. Firecracker: on servers you run or rent.
  • Cost model — WebContainers: effectively zero marginal cost; each user funds their own execution. Firecracker: you pay per host, per second of VM life; copy-on-write and scale-to-zero cut it, they don't eliminate it.
  • Language and runtime support — WebContainers: Node/JS-first, plus whatever has been ported to WebAssembly. Firecracker: anything that runs on Linux, because it is Linux.
  • Native binaries and system packages — WebContainers: no arbitrary ELF execution, no Docker, no apt, no stock Postgres or Redis binary. Firecracker: install and run anything, including compilers and databases.
  • Networking — WebContainers: fetch through the browser (CORS applies), inbound via a service-worker preview URL only; no raw sockets, no externally reachable port. Firecracker: real network namespace and tap device, raw sockets, real listening ports, reachable preview URLs.
  • Persistence and state — WebContainers: in-memory, lives and dies with the tab. Firecracker: CoW disk, memory snapshots, fork of a running machine (400–750ms same-host), durable volumes, hibernate and wake.
  • Headless / server-side use — WebContainers: needs a browser; a headless-Chrome workaround gives you a server bill without a kernel. Firecracker: headless by nature — cron jobs, queues, and autonomous agents just call an API.
  • Startup and offline — WebContainers: instant, nothing to schedule, works offline once loaded. Firecracker: p50 179ms via snapshot-restore (~3s for a template's first-ever cold boot), always requires the network.
WebContainers is compute at the edge of the user. A microVM is compute you own. Almost every real decision between them is just that sentence applied to your workload.

The two questions that decide it

Skip the feature matrices. Answer these in order and the choice usually makes itself.

  1. Does the code need real Linux? If it needs a native binary, a compiler, a system package, a database, a non-JS runtime, or a listening socket, in-browser execution is out — not because it's weak, but because there's no kernel for those things to talk to.
  2. Is there a human with a browser tab open the entire time the code runs? If yes, and the work fits in one machine's memory and patience, running it in their tab is free, private, and instant. If no — a queue, a cron job, a webhook, an agent — you need something server-side.
  3. Who should pay for the compute? If the answer is "the user, invisibly, on their own hardware," that's an argument for the browser that no server-side platform can match on price.

Great fits for WebContainers: interactive JS/TS tutorials, docs playgrounds, framework demos and starters, in-browser IDEs, "try it now" buttons, teaching environments, and anything where the value is a user editing code and immediately seeing it run.

Great fits for microVMs: AI agents running arbitrary shell commands, polyglot code interpreters, anything with native dependencies or a compiler, CI-shaped workloads on untrusted repos, per-tenant databases, data pipelines, and anything that must run headless, in the background, or for longer than a person is willing to sit and watch.

The AI-agent angle, specifically

If you're building an agent, question two answers itself: there is no tab. An agent working through a backlog overnight has no browser, no user, and no session. It also does the things that most need a kernel — installing packages, invoking compilers, running the test suite, occasionally writing a command whose intent is somewhere between "clean the build directory" and "remove everything, recursively, from the root, without asking." A container would treat that suggestion as a request to the host kernel it shares with everyone else. A microVM treats it as one guest's problem, and you throw the guest away.

The second agent-specific reason is state. Agents work by trying things, and the expensive part of a try is getting back to a warm state — repo cloned, dependencies installed, database seeded. `snapshot()` and `fork()` turn that into a branch operation: fork the warm sandbox three times, let three attempts run in parallel, keep the one whose tests pass. That's not a browser-tab shape at all; it's a fork-tree, and it needs a machine you own.

Watch for the hybrid trap. "Run it in the browser, and just proxy the parts that need a server" starts as one small proxy and ends as a full backend that you now maintain twice — once in WASM and once for real. If a meaningful share of your workloads need real Linux, pick the server-side path deliberately rather than arriving there by accretion.

When the microVM is the wrong answer

I'll say the quiet part: if you're building a docs playground, a framework tutorial, or an editable "try our API" widget in JavaScript, and you're currently spinning up server-side sandboxes for it, you are probably paying for infrastructure you could delete. WebContainers would give you lower latency, better privacy, no capacity planning, and a compute bill that stays flat as you grow. Serving one user's `npm run dev` from a rack in another country is a strange thing to do when they already have a computer.

The microVM only earns its cost when the workload genuinely can't live in a tab: it needs real Linux, it needs to run headless, it needs to outlive a session, it needs real networking, or it needs snapshot-and-fork over state. That's most agent infrastructure, most code interpreters that aren't JS-only, and essentially all CI. But it isn't everything — and a platform that only ever tells you to buy more servers isn't being honest with you. The best architecture is often both: the browser for the interactive, JS-shaped, human-in-the-loop path, and microVMs for everything that has to run when nobody is watching.

Frequently asked questions

What is the difference between WebContainers and Firecracker microVMs?

WebContainers runs a Node-compatible runtime compiled to WebAssembly inside the user's browser tab, with a virtual in-memory filesystem and a service-worker networking shim. There is no kernel and no server. Firecracker runs a real Linux guest kernel per sandbox on your servers, isolated by KVM hardware virtualization, so arbitrary binaries, system packages, compilers, databases, and real sockets all work. The practical split: WebContainers is compute at the edge of the user and costs you nothing per run; a microVM is compute you own and operate, but it is actual Linux.

Can WebContainers run Docker, Postgres, or native binaries?

No — and not because of a missing feature, but because there is no kernel in a browser tab to load and execute an ELF binary or serve real syscalls. That rules out Docker, stock Postgres and Redis binaries, system compilers, ffmpeg, and language runtimes that have not been ported to WebAssembly. Native npm dependencies that compile at install time, like node-gyp-based packages, are the usual place projects hit this wall. Capabilities do expand over time, so verify specifics against StackBlitz's docs; workloads needing real Linux belong in a microVM or container instead.

Can an AI agent use WebContainers to run code?

Only while a browser tab is open. WebContainers executes in the user's browser, so an autonomous agent working through a queue overnight — with no user, no screen, and no session — has nowhere to run it. Driving headless Chrome on a server to host it technically works, but you then pay for servers without gaining a kernel, which defeats the point. Agents that install packages, invoke compilers, run test suites, or execute model-written shell commands need a headless server-side sandbox; a Firecracker microVM gives that with its own guest kernel per run.

Is running untrusted code in the browser safe?

It is unusually safe, for a reason that is easy to miss: the code runs in the user's own browser sandbox, on the user's own machine. The blast radius of malicious code is the person who chose to run it, and the boundary containing it is the most heavily attacked and heavily patched sandbox in software. Compare that to server-side execution, where untrusted code lands on infrastructure you own next to other customers' data. The catch is that this safety only applies to workloads that can run in a tab at all.

When should I choose a microVM sandbox over an in-browser runtime?

Choose a microVM when the code needs real Linux — native binaries, system packages, compilers, non-JS runtimes, a real database, raw sockets, or an externally reachable port — or when it must run headless, in the background, or longer than a person will sit and watch. Also choose it when you need state primitives a tab cannot provide: memory snapshots, forking a running machine, durable volumes, hibernate and wake. Stay in the browser for interactive JS/TS playgrounds, docs demos, and tutorials, where per-user compute is free and startup is instant.

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.