Vibe Engines
YouTube
AI System Design

Design a Code Execution Sandbox

Learn AI system design by building a secure code execution sandbox for untrusted, LLM-generated (or user-submitted) code — the runtime behind AI agents that write and run code, online judges and…

The numbers to beatincode + inputsisolatedrun under limitsoutonly captured result

The whole design, in writing

Learn AI system design by building a secure code execution sandbox for untrusted, LLM-generated (or user-submitted) code — the runtime behind AI agents that write and run code, online judges and notebooks — step by step. An interactive guide covering why running code directly is catastrophic, isolation with containers/microVMs, resource limits, the execution pipeline, ephemeral sandboxes, warm pools for latency, and the unhappy paths (escapes, egress control, sessions).

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

Why is running code hard?

An AI agent writes code to solve a task and needs to run it. An online judge accepts a user's submission. A notebook executes a cell. In every case you're about to execute code you did not write and cannot trust — it could try to read secrets, attack internal systems, exfiltrate data, or hog the whole machine. And you have to do it fast, at scale, for many concurrent runs.

CallerLLM agent · user
New in this step: Caller.

A code execution sandbox runs untrusted code safely: strong isolation (so code can't touch anything it shouldn't), strict resource limits (so it can't exhaust the machine), ephemeral environments (a fresh sandbox per run, destroyed after), and a warm pool so it's still fast. Security and speed are the whole problem — the "run code" part is easy; doing it without getting owned is not.

What the new pieces do

Callerclient
An AI agent that wrote code to run (or a user submitting code to a judge/notebook). The code is untrusted by default.

Step 1 · Never run it directly

Untrusted code = full compromise

The naive approach: just exec the code in your service process (or on the host). Why is this catastrophic when the code is untrusted?

CallerLLM agent · user
The system as it stands at this step.

Run untrusted (LLM-generated) code directly in your service. What's the danger?

  1. Untrusted code executed in your process inherits its permissions and environment: it can read env vars/secrets, access the filesystem and other users' data, consume all CPU/memory, and make network calls to internal services. It is, by definition, arbitrary code execution on your infrastructure — a total compromise. It must run inside strong isolation with no ambient access.

  2. Syntax errors are harmless. The real danger is that untrusted code running with your privileges can do anything your service can — read secrets, exfiltrate data, attack internal systems, or take down the host.

  3. Slowness is a resource concern (handled by limits), but the primary danger is security: arbitrary untrusted code with your process's access is a full compromise, not just a performance issue.

Executing untrusted code in your process means it runs with your privileges and environment — it can read secrets and env vars, touch the filesystem and other users' data, consume all CPU/memory, and call internal services. That's arbitrary code execution on your own infrastructure: a complete compromise. The code must run somewhere with no ambient access to anything that matters — isolated from the host, the network, and every other run.

Why this piece earns its place

The trust boundary is set by who can influence the code, not by who wrote it. A program an agent emits was authored, in practice, by whatever text reached its context — a page it browsed, a ticket it was handed — so the employee who started the run is not its author, and no internal caller has earned a fast path around the sandbox. Ship one and it becomes the path everything routes through the first time a deadline argues for it. That also settles a deployment question that looks like taste. Execution belongs behind a service boundary, never a library the API imports, because a library runs inside whatever ambient authority the calling process was already handed: the database pool, the cloud role, the mounted config, the network position internal services trust by source address. Nobody adding the next feature to that service is thinking about your sandbox, and every one of them widens what it was meant to be sealed off from. And price the failure while it is still hypothetical: untrusted code that ran with a service's identity means every credential that identity could reach is now assumed public, so recovery is a rotation across all of them, not a rebuild of one host.

Step 2 · A service around execution

Submit, run isolated, capture

Wrap execution behind a service. What's the basic contract, regardless of the isolation tech?

CallerLLM agent · userExec APIsubmit · result
New in this step: Exec API.

An execution API takes code + language + inputs, runs it inside an isolated sandbox under strict limits, and returns only the captured result — stdout/stderr, return value, exit status, and metrics. Nothing else crosses the boundary: the sandbox has no access in (no secrets, no network) and only structured output goes out. This clean submit→isolate→capture contract is the frame; the next steps make the isolation and limits real.

Why this piece earns its place

Two fields decide whether a machine can actually drive this API. The first is output size. Captured output is the one thing that legitimately crosses the boundary, which makes it the one channel the resource limits do not cover — a program printing in a loop turns your result field into the exhaustion vector you closed everywhere else. Cap the bytes, truncate at the cap, and say in the response that you truncated, because silent truncation reads downstream as a program that stopped early. The second is the difference between the code failed and we failed to run the code. A non-zero exit, a compile error, a timeout: those are results, and the caller should act on them. No sandbox available, a host that died mid-run, a pool that came up empty: those are ours, and only those should be retried. Collapse them into one error field and an agent in a loop will retry a deterministic failure until its budget is gone. One thing the contract has to refuse outright is an input that is a reference. Accept a URL to fetch and the service dials it with its own network position and its own credentials, handing the caller exactly the reach this boundary exists to remove. Inputs arrive as bytes.

  • incode + inputs
  • isolatedrun under limits
  • outonly captured result

What the new pieces do

Exec APIgateway
Accepts code + language + inputs, schedules a run in an isolated sandbox, and returns captured output/result.

Step 3 · Contain what it can touch

Isolation: containers & microVMs

The code must run somewhere it can't harm anything. What actually provides that isolation, and how strong does it need to be?

SandboxmicroVM · no net
New in this step: Sandbox.

What isolates untrusted code from the host and other runs?

  1. You can't review arbitrary (or generated) code for every run, and cleverly malicious code can hide intent. Isolation must be enforced by the runtime, not by inspecting the program.

  2. A different user is a start but nowhere near enough — plain process isolation shares the kernel and can be escaped, and it doesn't block network or resource abuse by itself. You need strong sandboxing: containers hardened with syscall filtering, or microVMs.

  3. Untrusted code runs inside a container hardened with syscall filtering/user-namespacing, or a lightweight microVM that gives near-VM isolation with fast startup. Deny network by default, drop privileges, restrict the filesystem, and layer defenses (defense in depth) so a single weakness doesn't breach the host.

Run the code inside a hardened sandbox: a container with restricted syscalls (seccomp/gVisor-style filtering, user namespaces) or a lightweight microVM (Firecracker-style) that gives near-VM isolation with fast boot. Apply least privilege — no network by default, a minimal read-only-ish filesystem, dropped capabilities — and layer these (defense in depth) so no single weakness breaches the host. Isolation is enforced by the runtime, never by trusting the code.

Why this piece earns its place

Start with the endpoint everyone forgets. The cloud instance metadata service answers ordinary HTTP from inside the guest and hands the host's credentials to whatever asks, so it has to be blocked at the sandbox's network rather than merely left unused — a link-local address is not a secret, and the code has read the same documentation you did. Then two consequences of the choice itself. Hardware virtualization needs a host that offers it, so picking microVMs constrains which instance types you can buy and which regions you can run in, and that lands in capacity planning long before it reaches a security review. And the guest image becomes a versioned artifact you own, which makes it part of every answer you return. An interpreter or library bump changes what a program does, and rolling a new image across a pool is gradual by construction — the old sandboxes have to drain first — so two identical submissions minutes apart can land on different images and disagree. Stamp the image version on the result. It costs nothing now, and it is the only thing that will ever explain a judge's scores moving on a day nobody deployed anything.

  • container + seccomphardened, shared kernel
  • microVMnear-VM, fast boot
  • no net · least privdefense in depth

What the new pieces do

Sandboxdata
The isolated runtime (container/microVM) where untrusted code actually executes — restricted syscalls, no network by default.

Step 4 · Contain how much it consumes

Resource limits

Isolation stops code from reaching things — but code can still hang forever, allocate all memory, spawn endless processes (a fork bomb), or fill the disk, taking down the worker and co-located runs. How do you bound consumption?

Resource Limitscpu·mem·timeSandboxmicroVM · no net
New in this step: Resource Limits.

Enforce strict resource limits per run: a CPU quota, a memory cap, a hard wall-clock timeout, a process/thread limit (to stop fork bombs), and disk quotas — via cgroups/ulimits or the microVM's allocation. Anything exceeding its limits is hard-killed. This turns "runs forever / eats the machine" into "fails cleanly at its quota", protecting the worker and every other tenant. Isolation contains what; limits contain how much — you need both.

Why this piece earns its place

Sort these knobs by where they belong, because the answer differs for each. Wall-clock time belongs in the request — a judge's test case and an agent's exploratory loop want different deadlines — but clamped by a server-side maximum, since the caller is untrusted too. Memory is not the caller's to choose: it decides how many sandboxes fit on a host, and therefore your cost per run. Then learn what each limit actually does when it is hit, because they do not behave alike. A CPU quota throttles; it never kills, so a compute-bound run does not fail at that limit, it simply takes longer — and the thing that finally ends it is the wall clock. That couples the two: the same program finishes comfortably on a quiet host and times out on a busy one, which makes the deadline the least reproducible promise in the system and the source of most it-worked-yesterday reports. A garbage-collected runtime has the opposite shape. Sized to exactly the memory cap it does not slow down at the boundary, it dies there, so usable heap is always below the quota and the runtime needs its own smaller ceiling set inside the sandbox. The quiet payoff: only once every run has a ceiling does the fleet have a denominator, which is what turns Step 7's pool sizing into arithmetic instead of guesswork.

  • cpu · mem · timeper-run quotas
  • pid limitstop fork bombs
  • exceed → killfail cleanly

What the new pieces do

Resource Limitsservice
Enforces per-run quotas (CPU, memory, wall-time, processes, disk) and hard-kills anything that exceeds them.

Step 5 · The execution pipeline

Queue → sandbox → capture

Put it together into a flow that handles many concurrent runs and returns results. What's the path a submission takes?

Exec APIsubmit · resultJob Queuepending runsResource Limitscpu·mem·timeOrchestratorsandbox managerResultstdout · exit
New in this step: Job Queue, Orchestrator, Result.

Submissions flow through a queue to an orchestrator that assigns each to a fresh isolated sandbox (with limits wired up), runs the code, captures stdout/stderr/return/exit/metrics, returns the result, and tears the sandbox down. The queue absorbs bursts and lets you scale workers; the orchestrator manages sandbox lifecycle and enforces limits; only the structured result leaves. Long or interactive runs stream output; batch judge runs return when done.

Why this piece earns its place

A queue hands you one guarantee you did not have before: delivery is at-least-once. The visibility timeout has to exceed the longest wall-clock limit you allow, or the queue gives the same job to a second worker while the first is still running it. You pay twice, and anything with a side effect outside the sandbox — a package pulled through your mirror, an allowlisted API call — happens twice. So submit carries a caller-supplied run id, and the orchestrator refuses the duplicate rather than re-executing it, handing back the first attempt's answer. The second change is ownership of a live sandbox. Tear it down in a finally on the request path — that is what returns capacity fast enough for a pool to mean anything — but a finally only runs while the process holding it is alive, and an orchestrator that dies mid-run leaves a sandbox nobody owns: still allocated, still counted against the host, no request ever coming back for it. So in-path teardown needs a backstop: a lease on every sandbox, and a reaper walking the runtime's own inventory to kill whatever has no live owner or an expired lease. Without one the fleet looks healthy right up to the day it has no free capacity and no running work to explain where it went.

  • queueabsorb bursts
  • orchestrateassign + limit + capture
  • result onlycrosses the boundary

What the new pieces do

Job Queuebus
Buffers execution requests so bursts drain through the sandbox fleet at a sustainable rate.
Orchestratorservice
Assigns each run to a fresh isolated sandbox from the pool, wires up limits, and reclaims the sandbox after.
Resultdata
Captured stdout/stderr, return value, exit status and metrics — the only thing that leaves the sandbox.

Step 6 · Nothing persists

Ephemeral sandboxes

If a sandbox were reused across untrusted runs without resetting, one run could leave behind malware, state, or traps for the next. How do you prevent cross-run contamination?

CallerExec APIJob QueueResource LimitsOrchestratorSandboxResult
The system as it stands at this step. · swipe to pan the diagram

How do you stop one untrusted run from affecting the next in the same sandbox?

  1. Each run gets a clean sandbox that is thrown away after, so nothing an untrusted run does — files written, processes left, memory state — can persist to affect another run or user. Ephemerality is what guarantees isolation holds over time, not just per run.

  2. Manually cleaning a reused environment is error-prone and leaves subtle state (installed packages, background processes, kernel state) that untrusted code can exploit against the next run. A fresh, destroyed-after sandbox is the safe default.

  3. Untrusted runs can be adversarial precisely toward the next tenant. You must structurally prevent contamination with ephemeral, per-run sandboxes, not trust.

Make sandboxes ephemeral: a fresh environment per run (or per user session), destroyed afterward, never reused with dirty state. Nothing an untrusted run does — files, leftover processes, memory, installed packages — can persist to affect another run or user. For workflows that need continuity (an agent iterating, a notebook), keep a per-session sandbox that lives only for that session and is still isolated and disposable — persistence is scoped to one trust boundary, then wiped.

Why this piece earns its place

The session case is where this stops being free, and the cost is scheduling, not security. A session pins one sandbox to one host for as long as it lives, so the fleet stops being interchangeable capacity — you cannot drain that host to patch it without killing live sessions, which runs straight into the patched-hosts defense in Step 8. So a session gets a maximum lifetime and an idle timeout whatever the user is doing, and the client has to survive losing one: the real state belongs in the caller's transcript or an object store, with the sandbox holding a reconstructible copy rather than the only copy. Then look at what a session consumes. It holds its memory the whole time it is idle, and a notebook is idle for most of its life while a human reads what the last cell printed. Sessions are billed in memory-hours, not CPU-seconds, so a quota model written around execution time prices them at nearly nothing — and they quietly become the largest line item on a bill nobody can explain.

  • freshper run/session
  • destroyedafter
  • sessionisolated + disposable

Step 7 · Fast enough to use

Warm pools & scaling

Booting a fresh microVM/container per run adds startup latency, and an agent that runs code in a loop can't wait seconds each time. How do you keep it fast while staying ephemeral?

Resource Limitscpu·mem·timeOrchestratorsandbox managerSandboxephemeral · destroyedWarm Poolpre-booted
New in this step: Warm Pool.

Keep a warm pool of pre-booted, ready sandboxes so a run starts in milliseconds instead of waiting for a cold boot — hand one out, run, discard it, and replenish the pool in the background. Autoscale the pool and worker fleet on demand. MicroVMs like Firecracker are chosen partly because they boot in ~milliseconds, making fresh-per-run affordable. Balance reuse vs isolation carefully: pre-warming the environment is fine, but never reuse one that has already run untrusted code.

Why this piece earns its place

A pool is only warm for the environment you pre-warmed. Add a second language, then a common dependency set, and the pool partitions: each partition needs its own headroom, so idle capacity multiplies by the number of variants and the least-used language is always the one that boots cold. The way out is fewer base images with the language-specific layer mounted at hand-out, rather than a pool per permutation. The knob that matters is the refill rate, not the pool size. A burst drains the pool faster than boots replace it, and the failure is a cliff rather than an error — every start is fast until the pool empties, then every start pays a full boot. The mean start latency tracks that shift exactly, climbing with the share of requests that miss the pool, which makes it coincident rather than early: by the time it has visibly moved you are already booting for real traffic, and the tail moved first. Occupancy and time-to-refill are the leading pair, so alert on those. One rule falls out of pre-booting: a sandbox exists before anyone has been authorized to use it, so nothing tenant-specific can be baked in at boot. Credentials, inputs and mounts arrive at hand-out or not at all.

  • warm poolpre-booted, ready
  • ~ms startnot cold boot
  • autoscalematch demand

What the new pieces do

Warm Pooldata
Pre-booted sandboxes kept warm so a run starts in milliseconds instead of waiting for a cold VM boot.

Step 8 · The sharp edges

Escapes, egress & real workloads

Even with isolation and limits, the hard parts remain: sandbox escapes, controlling network egress (data exfiltration), letting real code install packages or access data it legitimately needs, and abuse.

CallerExec APIJob QueueResource LimitsOrchestratorSandboxWarm PoolResult
The system as it stands at this step. · swipe to pan the diagram

Assume escapes are possible and defend in depth — keep hosts patched, minimize the guest, run untrusted workloads on isolated infrastructure, and monitor. Control network egress tightly: default-deny, and if code needs the internet, allowlist/proxy it — uncontrolled egress means data exfiltration. Handle package installs / data access via a controlled path (a vetted mirror, mounted read-only inputs) rather than open network. Add rate limits, quotas and monitoring to stop abuse (crypto-mining, DoS). And log/observe runs (without weakening isolation) so you can detect and respond.

Why this piece earns its place

Default-deny has a hole teams leave open for years: name resolution. A sandbox that can reach a resolver can exfiltrate through it — the data leaves encoded in the names it asks about, and no connection you blocked is ever opened. The resolver belongs inside the allowlist, and the proxy should be the only thing in the guest's world that resolves anything. Nothing about that allowlist requires terminating TLS: an explicit forward proxy reads the target off the CONNECT line and dials that name itself, and a transparent one filters on the SNI. You terminate to read the payload, which is a separate decision. The split that matters is between those two proxies — an explicit one dials the name it allowed, while a transparent filter lets the guest pick the address and only reads a name off the handshake, so a permitted name can front a destination you never allowed. Allowlisting by address closes that and stops being enforceable the moment a permitted service sits behind a CDN whose addresses are shared with everything else. A successful escape raises nothing, so your highest-value telemetry is the refusals — blocked egress, denied syscalls, kills at quota — every one of which is recorded outside the sandbox by something the code cannot reach. What the code prints is attacker-controlled input to your logging pipeline: capture it as an opaque blob, never as a line something downstream will parse.

You did it

You just designed a code execution sandbox.

CallerExec APIJob QueueResource LimitsOrchestratorSandboxWarm PoolResult
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Running untrusted (LLM-generated or user) code directly is arbitrary code execution on your infra — a full compromise.
  • Wrap execution in a service: code in, isolated run, only captured result out — a one-way boundary.
  • Isolate with hardened containers (restricted syscalls) or microVMs, least privilege, no network by default.
  • Enforce resource limits (CPU/memory/time/processes/disk) and hard-kill overruns to stop hangs and fork bombs.
  • Pipeline: queue → orchestrator assigns a fresh sandbox under limits → capture output → destroy.
  • Make sandboxes ephemeral (fresh per run/session, destroyed) so nothing contaminates the next run.
  • Keep a warm pool for millisecond starts; control egress, package/data access, and abuse — defense in depth.

Deep cut · 29:08

Sealed room. Key still leaked.

The interactive build above lays out the sandbox: isolation with containers and micro VMs, resource limits, the execution pipeline, ephemeral rooms, warm pools and egress control. This film is a locked-room mystery. At 3:14 in the morning a cloud key is used by a stranger, and the code that leaked it never escaped its room. The film builds the sandbox one reasonable fix at a time, with the run loop written out line by line as pseudo code, then replays the night to find how the key got out.

  • See the leak: every wall held. The run loop handed each room a copy of the service's own settings, cloud key included, and every room had an open window. It never broke out. We handed it the key.
  • Take it into the interview: isolate each run (a micro VM with its own kernel, or a hardened container), set CPU, memory and time limits from outside, destroy every room instead of cleaning it, hide the build behind a warm pool, start every room with empty settings, and deny outbound traffic except through one proxy with a list.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why is running untrusted code directly in your service a full compromise, not just a bug risk?

    Because code executed inside your process or on your host inherits that context's privileges, environment, and access, so untrusted code effectively becomes you. It can read environment variables and secrets the service holds, touch the filesystem and thereby other users' data, open network connections to internal systems that trust your service's network position, consume all the CPU and memory on the machine, and generally do anything your service is permitted to do. That is the textbook definition of arbitrary code execution on your own infrastructure, which is the most severe class of security failure — the attacker isn't exploiting a narrow flaw, they're simply running whatever they want with your permissions. And the code genuinely is untrusted: LLM-generated code can be wrong or manipulated via prompt injection into doing something harmful, and user submissions to a judge or notebook can be deliberately adversarial. The consequence is that you cannot make this safe by inspecting or sanitizing the code, because you can't reliably analyze arbitrary programs and malicious ones hide intent; the only viable approach is to contain execution so that even fully malicious code has no ambient access to anything valuable — which is exactly what a sandbox provides.

  2. Isolation vs resource limits — why do you need both?

    They defend against two different attack surfaces, and either alone leaves you exposed. Isolation controls what the code can reach: it prevents untrusted code from accessing the host, the network, secrets, the filesystem, and other runs, by executing it inside a hardened container or microVM with restricted syscalls, dropped privileges, and no ambient access. But even perfectly isolated code that can't reach anything external can still harm you by consuming resources: an infinite loop pins a CPU forever, a runaway allocation exhausts memory, a fork bomb spawns processes until the scheduler collapses, and unbounded disk writes fill the volume — any of which crashes or starves the worker and every co-located run, a denial-of-service against the fleet. Resource limits control how much the code can consume: per-run quotas on CPU, memory, wall-clock time, process/thread count, and disk, enforced via cgroups and ulimits (or the microVM's allocation), with a hard kill for anything that exceeds its budget. So isolation contains the blast radius of what code can touch, and limits contain the blast radius of what it can consume; a secure sandbox needs both, because untrusted code can attack through reach or through exhaustion.

  3. Containers vs microVMs for isolating untrusted code — what's the tradeoff?

    Both run the code in an isolated environment, but they differ in the strength of the boundary and their startup cost. A container shares the host's kernel and isolates via namespaces and cgroups, which is lightweight and fast but means a single kernel vulnerability can become a container escape to the host; you harden this substantially with syscall filtering (seccomp), user namespaces, and runtimes like gVisor that interpose a user-space kernel to shrink the attack surface, but the shared-kernel model remains a theoretical weak point. A microVM (such as Firecracker) instead boots a tiny, minimal guest kernel inside a lightweight virtual machine, giving you near-full-VM isolation — the untrusted code runs against its own kernel behind a hardware-virtualization boundary — while still starting in milliseconds and using little memory, unlike traditional heavyweight VMs. The tradeoff is that microVMs add a bit more overhead and operational complexity than bare containers, in exchange for a materially stronger isolation boundary. In practice, high-risk multi-tenant execution of untrusted code (exactly the AI-agent and online-judge case) leans toward microVMs for that stronger boundary, and both approaches are combined with least privilege, no network by default, and defense in depth so no single weakness breaches the host.

  4. Why must sandboxes be ephemeral, and how do you still support stateful sessions?

    Ephemerality guarantees isolation across time, not just at a single moment. If a sandbox were reused across untrusted runs without being fully reset, one run could leave behind artifacts that compromise the next — malware or a modified interpreter, background processes, poisoned files or installed packages, or subtle state — turning a shared environment into a channel for one tenant to attack another. Manually cleaning a reused environment is error-prone and can't reliably erase everything (kernel state, lingering processes, cached data), so the safe default is to give every run a fresh sandbox and destroy it afterward: a clean environment in, a discarded environment out, which makes cross-run and cross-tenant contamination impossible by construction. For workflows that genuinely need continuity — an agent iterating on code across several steps, or a notebook where later cells depend on earlier ones — you don't abandon this; instead you scope persistence to a single trust boundary by keeping a per-session sandbox that lives only for that one user's session, remains fully isolated and resource-limited throughout, and is torn down and wiped when the session ends. So state is allowed, but only within one user's isolated, disposable session, never reused across different untrusted callers.

  5. Why is controlling network egress so important, and how do you handle code that legitimately needs the network?

    Network egress is the primary channel for data exfiltration, so leaving it open undermines much of your isolation. Even if code can't escape the sandbox, if it can freely reach the internet it can send out anything it managed to read or compute — and for LLM-generated code that may have been steered by prompt injection, or for adversarial submissions, uncontrolled outbound access also enables attacks on internal systems, participation in DoS, or crypto-mining. So the default must be deny-all egress: the sandbox has no network unless explicitly granted. The challenge is that real workloads sometimes legitimately need external resources — installing a package, fetching a dataset, calling an allowed API — and the wrong fix is to open the internet. Instead you provide controlled paths: route any needed access through an allowlist or an egress proxy that permits only specific, vetted destinations; serve package installs from a vetted internal mirror rather than the open registry; and mount required input data into the sandbox as read-only rather than having the code fetch it over the network. This way the code gets exactly the access it needs through channels you audit and constrain, while arbitrary outbound traffic — the exfiltration and abuse vector — stays blocked. Combined with rate limits, quotas, monitoring, and defense-in-depth against escapes, egress control is a core part of keeping untrusted execution contained.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Running untrusted (LLM-generated) code directly is dangerous because…

    Untrusted code with your access can read secrets, exfiltrate data and attack internal systems.

  2. Isolation for untrusted code is provided by…

    The runtime enforces containment (seccomp/gVisor or Firecracker), never trust in the code.

  3. Resource limits exist to stop…

    Isolation contains what code touches; limits (CPU/mem/time/pids) + a hard kill contain how much it consumes.

  4. Sandboxes are ephemeral so that…

    Fresh per run/session, destroyed after — cross-run contamination is impossible by construction.

  5. Fresh-per-run stays fast by using…

    Pre-warm clean environments for millisecond starts; never reuse one that already ran untrusted code.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Submit & run: accept code + language + inputs, execute in an isolated sandbox, return only the captured result.
  • Isolate: run untrusted code with no ambient access to the host, network or other runs.
  • Bound consumption: cap CPU, memory, wall-time, processes and disk, hard-killing overruns.
  • Stay clean: a fresh sandbox per run/session, destroyed afterward — no cross-run contamination.
  • Stay fast: a warm pool of pre-booted sandboxes for millisecond starts.

The qualities that shape everything

Each one names the mechanism that buys it.

Only structured output ever escapes
A one-way submit→isolate→capture boundary: no ambient inputs (no secrets, no network) in, only captured stdout/exit/metrics out.
Untrusted code can’t reach anything valuable
A hardened container (restricted syscalls, user namespaces) or a lightweight microVM, with least privilege and no network by default — enforced by the runtime, never by trusting the code.
One job can’t exhaust the fleet
Per-run CPU, memory, wall-time, process and disk quotas via cgroups/ulimits, hard-killing anything that exceeds them.
No cross-run or cross-tenant contamination
Ephemeral sandboxes — a fresh environment per run/session, destroyed afterward — so nothing an untrusted run leaves behind persists.
Fresh-per-run stays low-latency
A warm pool of pre-booted clean sandboxes hands one out in milliseconds and replenishes in the background; fast-booting microVMs make it affordable.
No data exfiltration or unchecked escapes
Default-deny network egress with allowlist/proxy for legitimate needs, plus defense in depth (patched, minimal, isolated hosts) and monitoring.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Strong isolation over running code directly in your process

Untrusted code in your process inherits your privileges — it can read secrets, touch other users’ data, exhaust the machine and reach internal systems. That’s arbitrary code execution on your own infra; it must run with no ambient access.

A hardened container or microVM over just running as a non-root user

Plain process isolation shares the kernel, can be escaped, and blocks neither network nor resource abuse on its own. Restricted syscalls or a lightweight microVM, plus least privilege and no network, is what actually contains untrusted code.

Ephemeral, destroyed-after sandboxes over reusing one sandbox and cleaning between runs

Manual cleanup leaves subtle state — installed packages, background processes, kernel state — an untrusted run can weaponize against the next. A fresh sandbox destroyed afterward makes cross-run contamination impossible by construction.

A warm pool of pre-booted sandboxes over cold-booting a sandbox per run

Booting a fresh microVM/container per run adds startup latency an agent looping over code can’t absorb. Pre-booting clean sandboxes gives millisecond starts while staying ephemeral — pre-warm the environment, never reuse a dirty one.

Default-deny egress (allowlist/proxy) over open network access

Uncontrolled outbound access means data exfiltration even when the code can’t escape the sandbox. Deny egress by default and route legitimate needs through an allowlist/proxy, a vetted mirror, or read-only mounted inputs.

The answer, out loud

What a strong answer to “Design a Code Execution Sandbox” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Pin down who’s calling, and why it’s scary

    Before I draw anything, I want to agree on who calls this. An AI agent writes code and needs to run it; an online judge grades a user’s submission; a notebook runs a cell. In every case it’s code we didn’t write and can’t trust — it might try to read secrets, reach internal systems, send data out, or hog the machine. The number callers feel is start time, because an agent running code in a loop can’t wait seconds each time. So the real question is: how do I run hostile code fast, at scale, without getting owned?

  2. 3–8 min

    Never in my process: a one-way contract

    The naive version is to execute the code inside my own service, and that’s the first thing I rule out: the code inherits my privileges, so it can read my secrets, touch other users’ data, eat all the CPU and call internal services. That’s arbitrary code execution on my own infrastructure, and I can’t sanitize my way out of it. So the skeleton is an execution API with a one-way contract: code, language and inputs go in; only the captured result comes out — what it printed, its exit status, metrics. Nothing goes in by default: no network, and every sandbox starts with empty settings, never a copy of the service’s own, which can carry a cloud key.

    Built in step 2: Submit, run isolated, capture
  3. 8–15 min

    Contain what it can touch

    So what actually contains it? I’d rule out anything that has to read the program first — I want a boundary that holds for code nobody has looked at. That leaves two shapes. A container is a process the host kernel keeps in its own namespaces, so the kernel is the boundary; I’d narrow it with syscall filtering, seccomp or gVisor. A microVM like Firecracker gives each run its own tiny kernel behind a hardware boundary and still starts in milliseconds, at more overhead and operational complexity. For untrusted multi-tenant code I’d take the microVM, because with a container I’m betting the host kernel has no exploitable bug, and care can’t win that bet. Either way the run gets no network and no privilege it wasn’t handed.

    Built in step 3: Isolation: containers & microVMs
  4. 15–20 min

    Contain how much it consumes

    Isolation controls what code can reach, not how much it consumes. An infinite loop, a runaway allocation or a fork bomb — a program that keeps copying itself — can still starve the worker and every job beside it. So each run gets a budget enforced from outside the code: a CPU quota, a memory cap, a wall-clock timeout, a process limit and a disk quota, through cgroups and ulimits or the microVM’s own allocation. Exceed any of them and the run is hard-killed, so “runs forever” becomes “fails cleanly at its quota.”

    Built in step 4: Resource limits
  5. 20–24 min

    Queue, orchestrate, capture, destroy

    Put together, a submission goes into a job queue, which absorbs bursts and decouples demand from a finite sandbox fleet. An orchestrator pulls each job, assigns it a fresh sandbox with its limits wired up, runs it, captures the output, returns the result and tears the sandbox down. Only the structured result crosses back. Long or interactive runs stream their output; batch judge runs return when they’re done.

    Built in step 5: Queue → sandbox → capture
  6. 24–29 min

    Fresh every time

    That teardown matters. The tempting shortcut is one long-lived sandbox cleaned between runs, but cleanup misses subtle state — installed packages, background processes, kernel state — that one run can weaponize against the next. A fresh sandbox, destroyed afterward, makes cross-run contamination impossible by construction. Agents and notebooks that need continuity get a sandbox scoped to one user’s session: isolated and limited throughout, then wiped. State is allowed, but only inside one trust boundary.

    Built in step 6: Ephemeral sandboxes
  7. 29–33 min

    And still fast enough to loop on

    The bill for fresh-every-run is a boot on every run, and an agent iterating on code can’t absorb that. So I’d keep a warm pool of pre-booted, clean sandboxes: hand one out in milliseconds, throw it away after, refill in the background, autoscale with demand. Warm, never reused — I’m pre-warming the environment, not recycling one that has already run untrusted code. MicroVMs are on the table partly because they boot fast enough to make that affordable.

    Built in step 7: Warm pools & scaling
  8. 33–37 min

    Assume the walls fail

    Two sharp edges remain. First, egress — outbound network traffic. Code that never escapes can still send out whatever it read if the network is open, so egress is denied by default: a legitimate need goes through a proxy with an allowlist, packages come from a vetted internal mirror, and input data is mounted read-only. Second, escapes. I assume one is possible and defend in depth — patched hosts, a minimal guest, untrusted work on isolated infrastructure — plus rate limits, quotas and monitoring against abuse like crypto-mining.

    Built in step 8: Escapes, egress & real workloads
  9. 37–42 min

    What I’d watch, and how it fails

    What I’d watch is capacity and kill rate: queue depth, time spent waiting for an orchestrator, warm-pool occupancy, and the share of runs that hit a limit instead of finishing, split by caller. That last one matters most — when one caller’s kill rate climbs, it’s usually the submitted code that changed, not my platform. The two failures I’d prepare for need different responses. If the pool can’t keep up, I shed load at the API: queue it, make callers wait, stop opening new sessions. If isolation is in question — a CVE in the runtime, a run that looks like it got out — the lever I want drains untrusted work off the fleet, and it has to exist before the day I need it.

  10. 42–45 min

    Close on the trade-off

    To close, in one breath: a one-way execution API, a queue and an orchestrator, a hardened container or microVM per run under hard limits and destroyed afterward, a warm pool to keep it fast, and default-deny egress. Every knob trades speed against containment. With more time I’d dig into stateful sessions for agents and notebooks, and the package-install path — real code needs dependencies, and that’s where the pressure to open the network comes from.

What this teaches

Learn AI system design by building a secure code execution sandbox for untrusted, LLM-generated (or user-submitted) code — the runtime behind AI agents that write and run code, online judges and notebooks — step by step. An interactive guide covering why running code directly is catastrophic, isolation with containers/microVMs, resource limits, the execution pipeline, ephemeral sandboxes, warm pools for latency, and the unhappy paths (escapes, egress control, sessions).

Key takeaways

  • Running untrusted (LLM-generated or user) code directly is arbitrary code execution on your infra — a full compromise.
  • Wrap execution in a service: code in, isolated run, only captured result out — a one-way boundary.
  • Isolate with hardened containers (restricted syscalls) or microVMs, least privilege, no network by default.
  • Enforce resource limits (CPU/memory/time/processes/disk) and hard-kill overruns to stop hangs and fork bombs.
  • Pipeline: queue → orchestrator assigns a fresh sandbox under limits → capture output → destroy.
  • Make sandboxes ephemeral (fresh per run/session, destroyed) so nothing contaminates the next run.
  • Keep a warm pool for millisecond starts; control egress, package/data access, and abuse — defense in depth.

Concepts covered

  • Why is running code hard?
  • Untrusted code = full compromise
  • Submit, run isolated, capture
  • Isolation: containers & microVMs
  • Resource limits
  • Queue → sandbox → capture
  • Ephemeral sandboxes
  • Warm pools & scaling
  • Escapes, egress & real workloads
RUN IT YOURSELF

The two-instruction program that passes every admission check

A tiny interpreter runs the same two hostile programs under the two policies step 4 argues about: a quota checked when the job is admitted, and a fuel counter decremented per instruction. It prints instructions executed, cells held and whether the sandbox ever came back. Change FUEL at the top, hit Run, and watch which limit fires first.

HOW TO READ THE CODE — 5 IDEAS
  1. Admission measures the job: spin is 2 instructions, so any quota a platform would actually set admits it — and then it never returns (step 4). Every admission word in the output is printed from admit(), so lowering ADMIT_QUOTA past 3 changes the sentence and the table instead of contradicting them.
  2. The fuel counter measures the execution, so the run stops at exactly FUEL instructions and the sandbox is reclaimed (step 5).
  3. Fuel bounds time, not space: double it and leak holds double the cells. A time limit silently picks a memory limit, and picks it wrong.
  4. Under a 256-cell cap leak dies at instruction 769 — the memory cap fires and the 1200 fuel never does. Two limits, both needed (step 4).
  5. A CPU quota only throttles. What decides the finite job's fate is the share of its deadline it already spends idle — 45% here, so it dies at 2 co-tenants. Raise SPEED and that fraction drops until nothing crosses at all: same mechanism, no kill. The deadline is the least reproducible promise in the system (steps 4 and 7).
CPython · WebAssembly
built to run code it has every reason to distrust — make the calls, drop the isolation, run the gauntlet.
Finished this one? 0 / 65 AI System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More AI System Designs