AI Agent Code Execution Sandbox: Firecracker, gVisor, Wasm and Containers Compared (2026)
An agent that can write and run code is an agent that can run code written by a stranger. The stranger is not the model itself. It is whoever managed to put text in front of the model: a web page the agent browsed, a README it cloned, a tool result, a support ticket, a PDF. Once that text steers the model, the model’s output is attacker-influenced code, and your infrastructure is about to execute it with whatever privileges you handed the runtime.
That is why an ai agent code execution sandbox is no longer an optional hardening step but the central design decision in any coding or data-analysis agent. The choice of isolation primitive decides your blast radius, your cold-start latency, your cost per session, and how much of your threat model you can honestly defend. A plain container is fast and cheap but shares a kernel with the host. A microVM gives you a hardware boundary at a modest price. A user-space kernel such as gVisor sits between them. WebAssembly offers a tiny, capability-based runtime but a narrow compatibility envelope.
This post gives you a threat model for untrusted code execution from an LLM, an honest look at each isolation layer with verified figures, and the surrounding controls (egress, filesystem, snapshots, secrets) that matter as much as the boundary itself.
What this covers: the threat model, the isolation spectrum, startup and density trade-offs, network and secret handling, a decision matrix, and configuration examples you can adapt.
Context and Background
Code execution became a first-class agent tool in 2023 with “code interpreter” features, and by 2026 it is the default substrate for coding agents, data-analysis agents, browser agents that run scripts, and evaluation harnesses. The pattern is always the same: the model emits a snippet or a shell command, a tool executes it somewhere, and stdout, stderr and files flow back into the context window. The “somewhere” is the sandbox.
Two things changed the stakes. First, agents now run for minutes or hours and install packages, clone repositories and call APIs, so the sandbox is no longer a throwaway calculator but a miniature workstation. Second, agents are wired to other systems through protocols such as the Model Context Protocol. Our analysis of MCP server security, tool poisoning and the confused deputy problem shows how an injected instruction in a tool description can steer an agent; the sandbox is what limits the damage when that steering succeeds.
If you have read our earlier comparison of Firecracker, gVisor and Kata for agent sandboxes, this article takes a different angle. That post focuses on the three Linux isolation runtimes. Here we start from the threat model, add WebAssembly and plain hardened containers to the spectrum, and spend as much time on the controls around the boundary (egress policy, filesystem and snapshots, secrets) as on the boundary itself. For the managed-versus-self-hosted question specifically, see our piece on managed sandboxes versus self-hosted in the OpenAI Agents API.
A caution on sources. Vendor documentation changes quickly, and several managed sandbox products do not publish their full isolation internals. Where this article states a number, it comes from the Firecracker NSDI 2020 paper, the Firecracker specification, or the gVisor documentation, and is attributed. Where a figure is an example to make arithmetic concrete, it is labelled illustrative.
The foundational reference for the microVM approach is the paper by Agache et al., Firecracker: Lightweight Virtualization for Serverless Applications, which describes the design that now underpins a large share of the managed sandbox market.
Reference Architecture: Threat Model and the Layers Around the Boundary
The direct answer: a sound ai agent code execution sandbox has five layers, not one. An isolation boundary (container, user-space kernel, microVM or Wasm runtime) contains the code. A control plane creates and destroys sandboxes. An egress proxy governs the network. A scoped filesystem plus snapshots governs state. A secret broker keeps credentials out of the guest. Remove any layer and the isolation boundary alone will not save you.

Figure 1: Reference architecture. The agent never talks to the guest directly; the orchestrator brokers execution, egress and secrets.
Figure 1 shows the shape. The agent runtime (the process hosting the LLM loop) holds no execution capability of its own. It sends a request to an orchestrator, which allocates a sandbox from a warm pool or a snapshot, streams the command in, and streams output back. The sandbox has no route to the internet except through an egress proxy, and it never holds long-lived credentials; a broker injects short-lived, narrowly scoped tokens at the proxy rather than in the guest’s environment.
Who the attacker is
Classic multi-tenant isolation assumes the tenant is a customer who might be malicious. Agent sandboxes have a subtler adversary. There are three distinct threat actors, and each wants something different.
The first is the injected instruction author: someone who planted text in content the agent reads. They want the agent to exfiltrate data (environment variables, files, source code, tokens), pivot into internal services, or burn compute. They do not need a kernel exploit. A curl to an attacker-controlled host with the contents of ~/.aws/credentials is enough if the credentials exist and the network is open.
The second is the hostile tenant: a user of your platform who deliberately asks the agent to run exploit code, looking for a container escape, a neighbour’s data, or the host’s cloud metadata endpoint. This is the adversary classic sandboxes were built for.
The third is the accident: the model, with perfectly innocent intent, runs rm -rf in the wrong directory, forks a bomb, fills the disk, or installs a package with a malicious post-install script pulled in by a typo-squatted name. Accidents outnumber attacks by orders of magnitude, and a sandbox that only defends against kernel exploits ignores the most likely failure.
Assets and what “escape” means
Define escape broadly. Kernel-level escape (the guest reaches host kernel memory) is the dramatic case, but the practical losses usually sit above it:
- Data in the guest: user files, repository contents, and any secrets present in environment variables or mounted volumes.
- Network reach: cloud instance metadata services (for example the link-local address commonly used for instance credentials), internal APIs, databases, and other tenants’ sandboxes on a flat network.
- Resource abuse: CPU, memory, disk, process table, and egress bandwidth, including cryptomining and use as a proxy for attacks on third parties.
- Persistence across sessions: poisoned caches, modified base images, or writable shared volumes that carry one session’s compromise into the next.
- The control plane: the orchestrator’s own credentials and APIs, if the guest can reach them.
The honest framing is that isolation primitives only address one row of that list, the host kernel and neighbouring tenants. Everything else is policy you build around it. The gVisor documentation says this directly: “a sandbox is not a substitute for a secure architecture.”
Control plane versus data plane
Split the system in two. The control plane is trusted: it authenticates the caller, chooses an image, allocates the sandbox, applies resource limits and network policy, issues scoped credentials and enforces lifetimes. The data plane is the untrusted guest and its virtual devices. Keep the control plane off the guest’s network, give the guest no credentials to it, and communicate over a narrow channel such as a vsock (virtual socket) or a stdio pipe to an in-guest agent process.
That narrow channel is itself attack surface. The in-guest agent that receives commands parses input from the orchestrator, but, importantly, returns output that the orchestrator must treat as hostile: size-limit it, strip control sequences, and never evaluate it. A guest that can crash or confuse the orchestrator through crafted output has escaped in a meaningful sense.
The isolation spectrum
The boundary choices fall on a spectrum of how much code between the guest and the host kernel you must trust.
Hardened containers share the host kernel. Isolation comes from namespaces, cgroups, capability dropping, seccomp filters and mandatory access control (AppArmor or SELinux). The attack surface is the full host syscall interface, which in modern Linux is hundreds of system calls reaching an enormous codebase. A kernel bug reachable by an unprivileged process is a container escape. Our guide to Kubernetes user namespaces and container escape hardening with seccomp and RuntimeClass covers how far you can push this model, and where it ends.
User-space kernels (gVisor) interpose a second kernel. gVisor’s Sentry, written in Go, implements Linux system calls itself, so no guest syscall passes straight to the host. The Sentry then uses a deliberately small set of host syscalls. The attack surface moves from “the Linux kernel” to “the Sentry plus a minimal host interface.”
MicroVMs (Firecracker, Cloud Hypervisor, Kata with a lightweight VMM) put the guest behind hardware virtualization. The guest runs its own Linux kernel; the host exposes a handful of virtual devices through a minimal virtual machine monitor (VMM). The attack surface is the VMM’s device emulation plus the hypervisor (KVM). That is smaller and better audited than the full syscall interface, but a real kernel still runs inside, so the guest has full Linux compatibility.
WebAssembly avoids the kernel question differently. A Wasm module runs in a runtime with linear memory, structured control flow and no ambient access to files or sockets; it can only call functions the host imports. The attack surface is the runtime’s correctness plus whatever host capabilities you grant. Compatibility is the price: arbitrary Python with native extensions, pip install, and shell tools do not simply run.

Figure 2: What stands between guest code and the host kernel in each model. Fewer trusted layers is not always better; compatibility and operations matter too.
Figure 2 draws the paths. The key reading is not “microVM good, container bad” but “what must be correct for the boundary to hold.” For a container it is the entire host kernel’s syscall surface. For gVisor it is the Sentry. For a microVM it is the VMM and KVM. For Wasm it is the runtime and the capabilities granted.
Deeper Analysis: What Each Boundary Actually Costs and Protects
Containers: fast, dense, and honest about what they are
A container is a process with a restricted view of the system. Startup is process-creation fast, typically tens to low hundreds of milliseconds once the image is local, and density is limited mainly by memory. For an agent that runs a few lines of arithmetic, that is attractive. For an agent running code authored by an adversary, the shared kernel is the problem: every syscall the guest can make is a call into the host kernel.
You can narrow this. A tight seccomp profile can deny syscalls that reach rarely used, historically buggy subsystems. User namespaces map container root to an unprivileged host user. Dropping all capabilities, mounting the root filesystem read-only, setting no_new_privs, and using a non-root UID remove the easy wins. Container hardening also covers runtime vulnerability response; our write-up on containerd and CRI-O hardening and CVE response shows how often runtime-level flaws appear in practice.
What hardening cannot do is change the nature of the boundary. If the workload is your own code or lightly constrained expressions, a hardened container may be acceptable. If it is arbitrary code from an LLM reading untrusted content, treat the container as a convenience layer and add a stronger one beneath it.
gVisor: a second kernel in user space
gVisor’s design principle, from its security documentation, is that no system call passes directly to the host. Each supported call has its own implementation in the Sentry. Only common functionality is implemented, so specialised APIs such as extended attributes, raw sockets and many ioctls are not passed through. The set of host resources the Sentry can reach is explicitly enumerated: it runs in an empty mount namespace and cannot open arbitrary files or create arbitrary sockets. Unsafe Go code is confined to files with an unsafe.go suffix, CGo is disallowed in the Sentry, and the project documents continuous fuzzing.
File access goes through a separate process, the Gofer, which manages the container’s filesystem and passes file descriptors to the Sentry on request. gVisor also supports a mode called directfs where the Sentry holds some file descriptors directly; the documentation lists file opens under directfs and socket creation under host networking as conditional exceptions to the minimal host surface.
The Sentry needs a way to intercept the guest’s system calls, and gVisor offers three “platforms”:
- systrap intercepts syscalls using seccomp’s
SECCOMP_RET_TRAP, which makes the kernel raiseSIGSYSon the triggering thread. It has been the default since mid-2023 and is described as the better choice inside a VM or on hosts without virtualization support. - KVM uses the kernel’s KVM facility so the Sentry acts as both guest operating system and VMM. It performs best on bare metal; nested virtualization overhead makes it less attractive inside a cloud VM.
- ptrace uses
PTRACE_SYSEMU. It runs anywhere but has high context-switch overhead, is no longer supported, and is expected to be removed.
This matters for agent sandboxes because most are run inside cloud VMs, where nested KVM is either unavailable or slow. systrap is therefore the practical default for gVisor-based fleets, and you should benchmark your own workload on it rather than relying on bare-metal numbers.
The cost is syscall-heavy performance and compatibility. Every guest syscall is handled in user space by Go code, so workloads dominated by small file operations or network round-trips pay more than they would in a plain container. Some syscalls and /proc or /sys details are unimplemented; most Python data tooling works, but anything that probes for unusual kernel features can fail. gVisor does not publish a single “overhead percentage” because it depends heavily on workload, platform and filesystem configuration. Measure instead of assuming.
What gVisor does not cover, by its own threat model: hardware side channels are left to the host and platform, resource exhaustion is left to cgroups, and network policy is applied at the container level. In other words, you still need the rest of Figure 1.
Firecracker microVMs: a hardware boundary with a small VMM
Firecracker is a VMM built on Linux KVM, written in Rust and started from Google’s crosvm. According to the NSDI 2020 paper, it deliberately omits BIOS, PCI, USB, legacy devices and VM migration, and emulates only a handful of devices: virtio network and block, a serial console, and a partial i8042 keyboard controller. Block devices are used instead of filesystem passthrough to reduce host kernel exposure. The paper reports around 50,000 lines of Rust, which it contrasts with over 1.4 million lines for QEMU at version 4.2, and notes that QEMU can require up to 270 unique syscalls.
Each microVM is one Firecracker process, configured through a REST API on a Unix socket. A companion program, the jailer, wraps Firecracker before the guest starts: it applies a chroot, PID and network namespaces, drops privileges, and installs a seccomp-bpf profile. The paper states that the profile allows 24 syscalls (with argument filtering) and 30 ioctls, 22 of which KVM requires. That is defence in depth: even if an attacker compromised the VMM via a device-emulation bug, they land in a process with a very small syscall allowance.
Verified figures worth knowing:
| Metric | Figure | Source and caveat |
|---|---|---|
| Boot to guest user space | <= 125 ms | Firecracker specification; minimal kernel and rootfs, serial console disabled |
| Boot with production kernel and userland | as little as 150 ms | NSDI 2020 paper, section 5.4 |
| 99th percentile boot, 50 concurrent VMs | 146 ms (pre-configured) | NSDI 2020 paper |
| VMM memory overhead | <= 5 MiB (about 3 MB measured) | Specification and paper; 1 vCPU, 128 MiB guest |
| QEMU overhead for comparison | about 131 MB | NSDI 2020 paper |
| Creation rate | up to 150 microVMs per second per host | NSDI 2020 paper |
Two honest caveats. First, these are 2019 to 2020 era measurements on specific hardware with a purpose-built minimal guest. A real agent image with Python, a package cache and a dozen tools will not hit 125 ms from a cold boot; it is realistically a few hundred milliseconds to more than a second depending on kernel, init system and image size. I am not aware of a neutral published benchmark for agent images, so treat any single number you see in marketing with suspicion and measure your own. Second, the specification page itself marks several performance targets (such as guest CPU above 95 percent of bare metal and specific network throughput figures) as integration tests pending, so do not quote those as verified.
Firecracker’s real contribution to agent sandboxes is less the 125 ms boot than snapshot and restore. A microVM’s memory and device state can be saved and a new VM resumed from that state, skipping boot and warm-up entirely. This is what lets managed services hand out a pre-warmed Python interpreter with loaded libraries in tens of milliseconds, and it is the basis of “pause and resume” for long-running agent sessions. Snapshots bring their own risk, covered below.
The costs: you need KVM, which means bare-metal hosts or cloud instances that expose nested virtualization; guest kernels must be supplied and patched; there is no GPU passthrough in the minimal device model; and memory is statically assigned per VM unless you add balloon devices. Density is excellent for small guests but your guest’s own page cache and kernel memory sit on top of the VMM overhead. The paper’s example of 8,000 128 MB functions on a 1 TB server is a design target, not a measured result, and an agent sandbox with 1 to 2 GiB per session will fit far fewer.
Kata Containers: microVMs wearing a Kubernetes costume
Kata Containers runs each pod inside a lightweight VM and presents a standard container runtime interface, so Kubernetes can schedule it using a RuntimeClass. Kata can drive multiple hypervisors, including QEMU, Cloud Hypervisor and Firecracker, and places an agent process in the guest to manage containers. For teams already on Kubernetes, this is the lowest-friction route to hardware isolation: the orchestrator is just the Kubernetes scheduler and the sandbox is a pod.
The trade-off against raw Firecracker is flexibility versus overhead. Kata’s pod model, guest agent and storage plumbing add startup time and memory relative to a hand-tuned Firecracker setup. You also inherit Kubernetes’ pod startup path (image pull, CNI, admission). A pod-per-session design is workable for sessions measured in minutes; it is painful for sub-second tool calls unless you keep a warm pool of pods.
WebAssembly: capability-based by construction
The WebAssembly security model starts from fault isolation. A module runs in a sandbox separated from the host runtime; linear memory is an isolated region of fixed maximum size, zero-initialised, with bounds checks that trap on out-of-bounds access. Control flow is structured: direct calls target valid function entries, indirect calls are checked against type signatures, and a protected call stack guards returns. The WebAssembly security documentation is candid that this is coarse-grained control-flow integrity, and that data inside linear memory can still overwrite adjacent objects within the same region. It also notes that each module is subject to its embedding’s security policies.
For agents, the interesting property is that a module has no ambient authority. It cannot open a file or socket unless the host imports a function that does. With WASI (the WebAssembly System Interface), the host pre-opens specific directories and grants specific capabilities, which maps neatly to least privilege. Startup is typically microseconds to a few milliseconds for instantiation and memory overhead is small, which makes Wasm attractive when you run huge numbers of short tool calls.
The limits are real. Languages must compile to Wasm; Python runs via an interpreter compiled to Wasm (for example CPython or Pyodide builds), and native extensions such as NumPy need Wasm-compiled variants. There is no apt-get, no arbitrary subprocess, and threading, networking and filesystem support vary by runtime and WASI version. Wasm is a superb fit for a constrained “run this expression, this plugin or this small script” tool, and a poor fit for “give the agent a Linux workstation.” Our article on WebGPU and WebLLM in-browser inference discusses how Wasm sandboxing shows up on the client side, where the browser itself supplies the isolation.
Also remember that Wasm runtimes are software with bugs. Escape vulnerabilities have been found in production runtimes; the boundary is only as good as the runtime and its JIT. Defence in depth still applies: run the runtime inside a container or microVM if the code is truly hostile.
Startup latency and density, side by side
The table below summarises qualitative relationships. Numbers that are not sourced above are marked illustrative; use them for reasoning, not for capacity planning.
| Boundary | Cold start | Memory floor per sandbox | Syscall compatibility | Host attack surface |
|---|---|---|---|---|
| Hardened container | tens to hundreds of ms (illustrative) | few MB plus workload | Full | Whole host syscall interface |
| gVisor (systrap) | similar order to containers (illustrative) | tens of MB for Sentry (illustrative) | High, not complete | Sentry plus minimal host syscalls |
| Firecracker microVM | about 125 ms minimal; more with a real image | VMM <= 5 MiB plus guest kernel and RAM | Full (own kernel) | VMM devices plus KVM |
| Firecracker from snapshot | tens of ms (illustrative) | Guest RAM, can be shared copy-on-write | Full | Same as above |
| Kata pod | hundreds of ms to seconds (illustrative) | VMM plus guest kernel plus agent | Full | VMM plus KVM plus runtime shim |
| Wasm runtime | microseconds to ms (illustrative) | Linear memory only | Narrow, WASI subset | Runtime plus granted capabilities |
The broader unikernel and microVM design space is covered in our unikernels versus microVMs versus containers comparison, written from an edge perspective. The isolation trade-offs there carry over directly.
A worked cost example (illustrative)
Suppose an agent platform serves 2,000 concurrent sessions, each holding a sandbox with 1 GiB of guest RAM, and the average session runs code for 4 percent of its wall-clock time. A microVM per session needs roughly 2,000 GiB of guest memory plus about 2,000 times the per-VM overhead. At around 5 MiB per VMM that is only about 10 GiB of overhead, a rounding error; the dominant cost is the guest RAM you promised. A container design could overcommit memory because idle processes release pages, while a microVM guest holds its page cache unless a balloon reclaims it.
The conclusion is not “containers are cheaper.” It is that idle sessions dominate cost, so the winning design pauses idle sandboxes. A snapshot of a paused microVM costs disk, not RAM. If your sessions are 96 percent idle, snapshot-and-resume lets you provision for the 4 percent that is active, plus resume latency. That single design choice moves cost more than choosing between gVisor and Firecracker.

Figure 3: Sandbox lifecycle. Warm pools and snapshots hide boot cost; the egress proxy and secret broker sit on the data path, not in the guest.
Figure 3 walks through a typical session. The orchestrator takes a pre-warmed sandbox from a pool (or restores a snapshot keyed by session), the agent’s command runs through the in-guest agent, any outbound request is mediated by the egress proxy, and on idle timeout the orchestrator snapshots state and releases the compute.
The Controls Around the Boundary
Network egress: default deny, then allow by name
Most real agent incidents are exfiltration, not escape, so the network policy matters more than the choice between gVisor and Firecracker. The default must be deny. Then add the minimum: a package mirror, a specific API, a documentation site.
Layer the controls, because each fails differently:
- Host or VM level. Place each sandbox on its own network namespace or tap device with no route to anything except the egress proxy. Block the link-local metadata address, RFC 1918 ranges and the orchestrator’s subnet explicitly.
- DNS control. Run a resolver that answers only allowlisted names, logs every query, and rate-limits. DNS tunnelling is a classic exfiltration path when everything else is closed.
- L7 proxy. For HTTP and HTTPS, force traffic through a forward proxy that enforces a hostname allowlist. For HTTPS without interception, the proxy sees the SNI and the CONNECT target; that is enough for hostname allowlists but not for path-level rules.
- Cluster policy. In Kubernetes, a default-deny
NetworkPolicywith an egress rule to the proxy only. We covered rollout patterns in Kubernetes default-deny network policy and microsegmentation.
Allowlisting by hostname has a known weakness: an allowed host can itself be a relay. If you allow a general-purpose code host or a paste service, an attacker can push stolen data to an account they control. Prefer mirrors you operate for package installs, pin them, and treat allowlists for collaboration platforms as a data-exfiltration risk to be accepted explicitly, not a technicality.
A frequent design question is whether to permit package installation at all. Three workable patterns exist. Pre-bake: ship images with a curated set of libraries and disable the network entirely for most sessions. Mirror: allow only an internal package proxy that serves vetted versions. Staged: let installs happen in a first phase with network on and no user secrets, then snapshot, then run the agent’s code in a second phase with the network off. The staged model is strong because the dangerous period (arbitrary install scripts) and the sensitive period (touching user data) never overlap.
Filesystem, ephemerality and snapshots
Start from an immutable base image mounted read-only, with a writable overlay or scratch volume for each session. Cap the scratch volume with a hard quota, not a soft one, so a runaway write cannot fill the host. Mount user data separately, scoped to the one session, and mount it noexec and nosuid if you do not need to run binaries from it.
Ephemerality is the default safety property: destroy the sandbox at the end of a session and nothing persists. Persistence is then an explicit, auditable act, such as exporting named artefacts to object storage after scanning them, rather than an accident of a long-lived VM.
Snapshots deserve their own threat model, because they copy more than files:
- Secrets in memory. A memory snapshot captures everything in RAM, including tokens the guest held. Never snapshot a sandbox that holds live credentials, or treat the snapshot as being as sensitive as the credentials.
- Randomness and identity reuse. If many sandboxes resume from one snapshot, they share the same pseudo-random generator state, hostnames, machine IDs and TLS session keys unless you reseed on resume. Firecracker’s own documentation discusses this clone-safety concern for snapshots; confirm the guidance for your version and reseed explicitly.
- Time. Guest clocks jump on resume, which breaks token expiry checks and certificate validation unless the guest resynchronises.
- Cross-tenant leakage. Memory pages restored copy-on-write from a shared base are safe only if the base was prepared without any tenant data. Build base snapshots from a clean pipeline and sign them.
- Disk-only snapshots (filesystem snapshots) avoid the memory issues and are cheaper to store. Modal’s documentation recommends filesystem snapshots for work that outlives the default sandbox lifetime, which is a 5-minute default maximum that can be raised to 24 hours with the
timeoutparameter.
A practical pattern: take a memory snapshot of a pristine, credential-free warm sandbox to eliminate boot time, and take filesystem snapshots of a user’s work to persist state between sessions. Never mix the two.
Secrets: the guest should not have them
The strongest secret policy is that the sandbox never holds a usable long-lived credential. Several patterns achieve this, in descending order of strength:
- Proxy-side injection. The egress proxy attaches the
Authorizationheader for allowlisted API hosts. The guest makes an unauthenticated request; the proxy adds the token. Exfiltrating the guest’s memory and files yields nothing. The cost is that the proxy must understand each API’s auth scheme and you cannot do this for end-to-end encrypted protocols you cannot terminate. - Short-lived, narrowly scoped tokens. Issue a token that expires in minutes, is restricted to one repository or one bucket prefix, and is bound to the session. Workload identity federation lets the orchestrator mint these from an identity assertion rather than storing static keys. Modal exposes this style via an option that injects an OIDC identity token into the sandbox environment; that token is still readable by the guest, so scope what the relying party accepts.
- Environment variables or mounted files. The weakest form, visible to every process in the guest and to any injected instruction that runs
env. If you must, make the credential low-privilege and short-lived, and assume it will leak.
Whatever the mechanism, assume the model’s context window and logs are also a leakage path. Redact secrets from tool outputs before they re-enter the prompt, and keep audit trails of what each session accessed. We cover the identity and logging side in AI agent audit logging and identity.
Resource limits and the kill switch
Every sandbox needs hard limits on CPU time, wall-clock duration, memory, process count, open files, disk and egress bytes. For Linux-based boundaries, enforce them with cgroups v2 on the host side, outside the guest’s control. For microVMs, Firecracker provides built-in rate limiters on block and network devices, implemented as in-memory token buckets with optional bursts, configured through its API. A watchdog in the orchestrator, not in the guest, must be able to kill a sandbox unconditionally. If the only way to stop a runaway process is to ask the in-guest agent politely, you do not have a kill switch.
Configuration Examples
The snippets below are minimal and meant to illustrate the shape of each approach. Check flag names and fields against the current documentation for your versions before deploying.
Kubernetes: RuntimeClass for gVisor and Kata, plus default-deny egress
A RuntimeClass (API group node.k8s.io/v1) maps a name to a CRI handler configured on each node. A pod opts in with spec.runtimeClassName. The overhead.podFixed field lets the scheduler account for the sandbox’s own cost, and scheduling constrains placement to nodes that actually have the runtime.
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
---
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata
handler: kata
overhead:
podFixed:
memory: "160Mi" # illustrative; measure your own
cpu: "250m"
scheduling:
nodeSelector:
sandbox.example.com/kata: "true" # your own node label
---
apiVersion: v1
kind: Pod
metadata:
name: agent-sandbox-7f3a
labels:
role: agent-sandbox
spec:
runtimeClassName: gvisor
automountServiceAccountToken: false
restartPolicy: Never
activeDeadlineSeconds: 900
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: runner
image: registry.example.com/agent-runner:2026-11
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
limits:
cpu: "1"
memory: 1Gi
ephemeral-storage: 2Gi
volumeMounts:
- name: scratch
mountPath: /work
volumes:
- name: scratch
emptyDir:
sizeLimit: 1Gi
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: agent-sandbox-egress
spec:
podSelector:
matchLabels:
role: agent-sandbox
policyTypes: ["Ingress", "Egress"]
ingress: []
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: egress-proxy
ports:
- protocol: TCP
port: 3128
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
Note automountServiceAccountToken: false: otherwise the pod carries a Kubernetes API credential that an attacker can use against your cluster. Also note that the DNS rule above should point at a filtering resolver in a real deployment, not the cluster DNS.
Firecracker: a minimal VM definition and jailer launch
Firecracker accepts a JSON configuration file. This example gives a read-only root filesystem, one vCPU, 512 MiB of RAM, and a single network interface whose tap device is wired to the egress proxy.
{
"boot-source": {
"kernel_image_path": "vmlinux-guest",
"boot_args": "console=ttyS0 reboot=k panic=1 pci=off ro"
},
"drives": [
{
"drive_id": "rootfs",
"path_on_host": "agent-rootfs.ext4",
"is_root_device": true,
"is_read_only": true
},
{
"drive_id": "scratch",
"path_on_host": "scratch-session-7f3a.ext4",
"is_root_device": false,
"is_read_only": false,
"rate_limiter": {
"bandwidth": { "size": 52428800, "refill_time": 1000 },
"ops": { "size": 2000, "refill_time": 1000 }
}
}
],
"machine-config": {
"vcpu_count": 1,
"mem_size_mib": 512
},
"network-interfaces": [
{
"iface_id": "eth0",
"host_dev_name": "tap-7f3a",
"rx_rate_limiter": {
"bandwidth": { "size": 10485760, "refill_time": 1000 }
},
"tx_rate_limiter": {
"bandwidth": { "size": 2097152, "refill_time": 1000 }
}
}
],
"vsock": {
"guest_cid": 3,
"uds_path": "/run/vsock-7f3a.sock"
}
}
Launch it through the jailer so the VMM runs chrooted, in its own namespaces, with dropped privileges and its seccomp profile:
jailer --id 7f3a \
--exec-file /usr/bin/firecracker \
--uid 20001 --gid 20001 \
--chroot-base-dir /srv/jailer \
--netns /var/run/netns/sbx-7f3a \
-- --config-file /vm-config.json
The --netns flag places the VMM in a pre-created network namespace, so the only interface it can reach is the tap you built. Pair that with host firewall rules on the tap, since the guest controls everything above layer 2.
Python: a thin orchestrator contract
Whatever boundary you pick, hide it behind a narrow interface so you can change it. The sketch below shows the contract: bounded runtime, bounded output, structured result, and a guaranteed teardown. The backend object is a placeholder for your Firecracker, gVisor or Kata driver.
from dataclasses import dataclass
import time
MAX_OUTPUT_BYTES = 64 * 1024
HARD_TIMEOUT_S = 30
@dataclass
class ExecResult:
exit_code: int
stdout: str
stderr: str
truncated: bool
timed_out: bool
duration_s: float
def run_untrusted(backend, session_id: str, code: str,
egress_profile: str = "none") -> ExecResult:
sandbox = backend.acquire(session_id, egress_profile=egress_profile)
started = time.monotonic()
try:
raw = sandbox.exec(
["python3", "-I", "-c", code], # -I: isolated mode, ignore env and cwd
timeout=HARD_TIMEOUT_S,
max_output=MAX_OUTPUT_BYTES,
)
out = raw.stdout[:MAX_OUTPUT_BYTES]
err = raw.stderr[:MAX_OUTPUT_BYTES]
return ExecResult(
exit_code=raw.exit_code,
stdout=out.decode("utf-8", "replace"),
stderr=err.decode("utf-8", "replace"),
truncated=len(raw.stdout) > MAX_OUTPUT_BYTES,
timed_out=raw.timed_out,
duration_s=time.monotonic() - started,
)
finally:
sandbox.destroy(force=True) # never leave the guest running
Treat everything in ExecResult as hostile when it returns to the prompt: it can contain prompt injection aimed at the model. A sandbox that contains the code but pipes its output straight into the next tool call has only solved half of the problem.
The Managed Sandbox Landscape
Building and operating this stack is real work: kernel images, snapshot pipelines, warm pools, fleet patching, noisy-neighbour control and abuse handling. Several vendors sell it as a service, and the managed market has grown quickly. What I can verify from primary documentation is limited, so treat the following carefully.
Modal’s Sandbox documentation states that you select a runtime with the runtime parameter, either "gvisor" or "vm"; the VM runtime runs the sandbox “in a virtual machine with its own Linux kernel,” and GPU sandboxes require the gVisor runtime. If the parameter is unset Modal chooses. Sandboxes have a default maximum lifetime of 5 minutes, adjustable up to 24 hours, plus an idle timeout. Secrets are passed through a secrets list. That is a useful data point: one vendor exposes both a user-space kernel and a VM boundary, which supports the thesis that no single boundary suits every workload.
For other products, including E2B, Vercel Sandbox, Cloudflare’s sandbox offerings and the managed sandboxes in agent SDKs, I was unable to verify isolation internals, limits or pricing from primary pages during research for this article. Commonly repeated claims that particular products use Firecracker should be confirmed against each vendor’s current documentation and security whitepaper before you rely on them. When evaluating any managed option, ask the same questions you would ask of your own build:
- What is the isolation boundary, and who patches the guest kernel and VMM?
- Is the sandbox single-tenant per session, and can sessions share a host with other customers?
- What is the egress model: default deny, allowlist, or open? Can you attach your own proxy?
- Are secrets injectable without entering the guest?
- How are snapshots stored, encrypted and isolated, and can you bring your own storage?
- What are the lifetime, CPU, memory and concurrency limits, and what does idle time cost?
- Where does the data live (region), and what is the audit trail?
Buy versus build mostly comes down to team size and fleet size. If you run fewer than a few hundred concurrent sessions and have no platform team, a managed sandbox removes a large operational burden at an acceptable cost. If you run thousands of concurrent sessions, need on-premises or sovereign deployment, or must integrate custom hardware attestation, a self-run microVM fleet pays for itself. For the highest assurance requirement, hardware-based memory encryption adds a further layer; see our explanation of confidential VMs with Intel TDX, AMD SEV-SNP and Arm CCA and remote attestation, which protects the guest from the host operator rather than the host from the guest.
Decision Matrix and Selection Flow

Figure 4: A selection flow. The first question is always who controls the code; the second is what the code needs from the operating system.
Figure 4 encodes the reasoning. Start from trust in the code, then from compatibility needs, then from latency and cost. A decision matrix gives the same logic in tabular form.
| Scenario | Recommended boundary | Why | Watch out for |
|---|---|---|---|
| Evaluating arithmetic or a restricted DSL emitted by the model | Wasm runtime (inside a container) | No ambient authority, microsecond startup | Compatibility; runtime bugs |
| Data-analysis agent running pandas on user files | gVisor or microVM, network off | Needs full Python stack, medium trust | Library support under gVisor; memory per session |
| Coding agent that clones repos and runs tests | Firecracker microVM or Kata, egress proxy | Arbitrary processes, package installs, long sessions | Snapshot hygiene; cost of idle sessions |
| Multi-tenant public platform, hostile users expected | microVM per session, single-tenant hosts for high risk | Hardware boundary plus jailer | Operational burden; guest kernel patching |
| Internal tool, code from your own developers only | Hardened container | Lowest overhead | Do not extend to untrusted content later |
| GPU workloads | gVisor or dedicated VM per job | GPU support limited in minimal microVM device models | Check vendor support; driver attack surface |
| Regulated data, need to hide data from the operator | microVM inside a confidential VM | Adds memory encryption and attestation | Complexity; performance variance |
Trade-offs, Gotchas, and What Goes Wrong
Escaping is rarer than leaking. Most post-mortems of agent incidents involve data leaving through a permitted channel: an allowed API, a DNS query, or the agent’s own output. A strong boundary with a lax egress policy is a locked door in a house with no walls.
Kernel updates are your job. A microVM does not remove the guest kernel from your patch cycle. A guest running a stale kernel is still compromised by a local privilege escalation, which matters if the guest holds the only secrets worth stealing. Rebuild images on a schedule, and prefer immutable images over in-place updates.
Warm pools leak state. Reusing a sandbox for a second session without full reset turns the first session’s leftovers into the second session’s attack surface. If you pool, pool pristine, never-used instances, and destroy after use.
Side channels remain. gVisor’s documentation explicitly leaves hardware side channels to the host and platform. Microarchitectural attacks cross microVM boundaries on shared cores. If your threat model includes a sophisticated co-tenant, use dedicated hosts or confidential computing and do not co-locate high-trust and low-trust sessions.
Nested virtualization is a silent tax. Running microVMs or the gVisor KVM platform inside a cloud VM can make boot times and syscall performance worse than documentation figures. Benchmark in the environment you will actually run.
The in-guest agent is attack surface. The process that receives commands inside the VM runs with high privilege and parses structured input. Keep it minimal, memory-safe where possible, and treat it as the first thing an attacker attacks after getting code execution.
Observability can leak. Verbose logs, tracing spans and replays often include the code, outputs and secrets. Apply the same redaction rules you apply to prompts. Our piece on OpenTelemetry GenAI semantic conventions for agent tracing discusses what to record and what to drop.
Anti-pattern: the sandbox with the production credentials. An agent that can reach your production database from a sandbox has not been sandboxed; it has been given a remote shell with extra steps. Run agents in an environment that holds only the data and rights needed for the task.
Practical Recommendations
Begin with the threat model and write it down: who can influence the model’s input, what the sandbox can reach, and what the worst plausible outcome is. Then choose the weakest boundary that meets that model, and put your effort into the controls around it.
For most teams building a coding or analysis agent in 2026, a defensible default is a microVM or gVisor boundary on a default-deny network, with a staged install phase, ephemeral filesystems, and proxy-side secret injection. Use gVisor where you are on Kubernetes, run inside cloud VMs without nested virtualization, and want container ergonomics. Use Firecracker or Kata where you need full kernel compatibility, long-lived sessions with snapshots, or the strongest practical isolation. Use Wasm for narrow, high-volume tool calls where compatibility allows. Use plain hardened containers only for trusted code.
Checklist to run before launch:
- Threat model written, including injected-instruction exfiltration as the primary scenario.
- Isolation boundary chosen and justified against the code’s trust level.
- Egress default deny, with DNS filtering and an L7 proxy; metadata endpoint blocked.
- No long-lived credentials in the guest; short-lived, scoped tokens or proxy injection.
- Read-only base image, hard-quota scratch space, destroy on completion.
- Memory snapshots only of credential-free pristine instances; reseed and resync on resume.
- Hard limits on CPU, memory, processes, disk, duration and egress, enforced outside the guest.
- Orchestrator-side kill switch tested, not just documented.
- Output treated as untrusted before it returns to the model.
- Image and kernel rebuild schedule, plus a plan for runtime CVEs.
- Audit log of every execution, tied to session and identity.
Frequently Asked Questions
What is the safest way to run code generated by an LLM?
Run it in a disposable microVM or gVisor sandbox with no network by default, a read-only base image, hard resource limits and no credentials inside. The isolation boundary stops kernel-level escape; the network, filesystem and secret controls stop the far more common exfiltration. No single technology is sufficient, and the right boundary depends on how much you trust the content the agent reads and the users who drive it.
Is a Docker container enough to sandbox an AI agent?
Only for trusted code. Containers share the host kernel, so a kernel vulnerability reachable from an unprivileged process can become a host compromise. Hardening with seccomp, dropped capabilities, user namespaces and read-only filesystems reduces risk but does not change the shared-kernel boundary. For code influenced by untrusted web content or hostile users, add gVisor, Kata or a microVM beneath it.
Firecracker or gVisor: which should I choose?
Choose Firecracker (or Kata) when you need full Linux compatibility, a hardware virtualization boundary, or memory snapshots. Choose gVisor when you are on Kubernetes, cannot rely on nested virtualization, or want container-like startup and density with a smaller attack surface than a plain container. gVisor trades some syscall-heavy performance and compatibility for a smaller host surface; Firecracker trades KVM requirements and guest kernel maintenance for stronger isolation.
How fast does a Firecracker microVM start?
The Firecracker specification claims boot to guest user space in 125 ms or less with a minimal kernel and root filesystem, and the NSDI 2020 paper reports as little as 150 ms with a production kernel and userland. A real agent image with Python and tools takes longer from cold, which is why managed services restore from memory snapshots or keep warm pools. Measure your own image on your own hardware.
Can WebAssembly replace containers for agent code execution?
For narrow cases, yes. Wasm gives capability-based isolation, tiny startup and low overhead, which suits evaluating expressions, running plugins and small scripts. It cannot yet replace a Linux environment for agents that install packages, spawn processes or use native libraries. Many teams run a Wasm runtime inside a container or microVM so a runtime bug does not become a host compromise.
How do I stop a sandboxed agent from leaking secrets?
Keep secrets out of the guest. Inject credentials at an egress proxy so the guest never sees them, or use short-lived, narrowly scoped tokens bound to the session. Default-deny the network, filter DNS, redact secrets from outputs before they re-enter the prompt, and never snapshot memory from a sandbox that held live credentials. Assume anything in the guest environment can be read by an injected instruction.
Further Reading
- Firecracker vs gVisor vs Kata for AI agent sandboxes for a runtime-by-runtime comparison.
- Nex N2.5 agentic model family and open-weights status, a case study of agentic models whose tool use makes sandboxing essential.
- Confidential VMs: Intel TDX vs AMD SEV-SNP vs Arm CCA for hardware-backed isolation and attestation.
- WebGPU and WebLLM in-browser inference for the browser and Wasm side of sandboxing.
- gVisor security model and gVisor platform guide, primary documentation.
- Firecracker: Lightweight Virtualization for Serverless Applications (NSDI 2020), the design paper.
By Riju — about
