all posts

What Coding Agents Actually Need From a Sandbox

Ajay Kumar··11 min read

Almost every sandbox comparison — including ones I have written — starts from the sandbox. Here is the isolation backend, here is the cold-start number, here is the pricing page, here is a table. It reads like a spec sheet because it is one, and spec sheets are a fine way to compare two things that do the same job. The problem is that you are not buying a sandbox. You are buying somewhere for a software-engineering agent to live, and agents have opinions about accommodation that do not appear on anyone's feature list.

So this post goes the other way round. Start from observed behaviour — the things open-source coding agents actually do over a real trajectory, the parts you notice when you watch a run at 2am — and derive the requirement from each one. The list comes out different. Some celebrated features turn out not to matter much; a few things nobody advertises turn out to be the whole game. Full disclosure: I build PandaStack, an open-source Firecracker microVM sandbox, so I have skin in this. The rule I have followed throughout is that the only concrete numbers here are our own measured ones; everything I say about anyone else is qualitative, and you should verify it against their current docs rather than my memory.

First, what agents actually do

Watch a session from any of the serious open-source software-engineering agents — SWE-agent, OpenHands, Aider, the various autonomous coding harnesses people wire up themselves — and the shape is remarkably consistent regardless of framework. The agent reads some of the repo. It runs the test suite and gets a wall of output. It edits three files. It reruns a subset of the tests. It installs a dependency it did not expect to need. It gets confused, reads a different file, tries again. Twenty to sixty turns later it either lands a patch or apologises.

Underneath that, the timing is lopsided in a way that matters for cost. Most of the wall-clock time is the model thinking; a much smaller slice is the sandbox doing anything at all. The state accumulated across those turns is load-bearing. The output volume is enormous. And the inputs — issue text, READMEs, web pages, dependency source — are frequently written by someone who is not you. Every requirement below falls out of one of those four facts.

1. Agents are mostly idle, so idle has to be nearly free

The behaviour: a coding agent spends most of a session waiting on an LLM. The sandbox sits there with a warm interpreter and a checked-out repo, doing nothing, while a model somewhere composes its next thought. Then it does 900ms of work and goes back to waiting. Duty cycle over a long session is low, and it gets lower as models get more thorough.

The requirement that falls out: per-second billing, and an idle cost close to zero without losing the workspace. Raw throughput — how fast the box compiles, how many cores you get — matters far less than most benchmark posts imply, because the agent is not compute-bound, it is model-bound. What actually shows up on the invoice is billing granularity and what happens during the gaps. A platform that bills you by the hour, or that requires an always-on instance to hold state, is charging you rent on a room where nothing happens.

For reference on our side: PandaStack bills per second at $0.054 per active vCPU-hour and $0.0162 per GiB-hour, and CPU is charged on CPU-seconds actually burned rather than on cores allocated. That is the shape you want, whoever provides it — the specific rates matter less than whether the meter stops when the agent stops. The wider argument is in /blog/always-on-vs-scale-to-zero-agent-infra, and the honest accounting of where an agent's money really goes is in /blog/ai-agent-cost-anatomy-tokens-vs-compute. Spoiler: it is the tokens. Which is exactly why paying rent on idle compute is such an irritating way to lose margin.

Test for this in one question: 'if my sandbox is alive for two hours and busy for four minutes, what do I pay?' Any answer involving 'two hours' is the wrong answer for an agent workload.

2. Agents lose the plot, so state must survive dozens of steps

The behaviour: at step nineteen the agent runs a script that imports a module it installed at step four, in a virtualenv it created at step two, against a file it edited at step eleven. It does not remember doing any of that, and it does not need to — it just expects the machine to still be the machine.

This is where stateless execution APIs quietly fail. A run-this-snippet-and-return endpoint means every step either rebuilds its own world or gets chained into one increasingly baroque shell one-liner. Agents handle that badly: they forget a step, the command grows, and eventually the model starts writing setup code instead of the fix. You need two distinct kinds of persistence and it is worth being precise, because vendors use the same word for both. A persistent filesystem workspace means the repo, the venv and the edits survive between calls. A live interpreter session means variables, imports and open handles survive — notebook semantics, a kernel that stays up. A sandbox can offer the first without the second, and for a data-heavy agent that difference is the whole ergonomics of the thing.

Here is that pattern with PandaStack's Python SDK — one sandbox for one task, a live code context on top of it, a timeout on every call, and teardown in a finally block so the machine dies even when your orchestrator does not get to the happy path.

from pandastack import Sandbox


class AgentWorkspace:
    """One machine for one task. State persists across steps; nothing leaks
    into the next task, because the machine is destroyed with the task."""

    def __init__(self, ttl_seconds: int = 1800):
        # ttl_seconds is the platform-side backstop: if this process dies
        # before close(), the sandbox still reaps itself. See requirement 4.
        self.sb = Sandbox.create(template="code-interpreter",
                                 ttl_seconds=ttl_seconds)
        # A live kernel. Variables and imports survive between agent steps.
        self.ctx = self.sb.create_code_context(language="python")

    def shell(self, cmd: str, timeout_seconds: int = 120) -> dict:
        """Filesystem-level work: clone, install, run the test suite."""
        r = self.sb.exec(cmd, timeout_seconds=timeout_seconds)
        return {
            "exit_code": r.exit_code,
            "stdout": _clip(r.stdout, 6000),
            "stderr": _clip(r.stderr, 2000),
        }

    def python(self, code: str, timeout_seconds: int = 120) -> str:
        """Session-level work: the agent's variables are still here."""
        ex = self.ctx.run_code(code, timeout_seconds=timeout_seconds)
        logs = ex.logs
        out = "\n".join(p for p in (logs.get("stdout", ""),
                                    logs.get("stderr", "")) if p)
        return _clip(out, 6000) or "(no output)"

    def close(self) -> None:
        self.ctx.close()
        self.sb.kill()


def _clip(text: str, limit: int) -> str:
    """Truncate loudly, so the model knows it is not seeing everything."""
    if len(text) <= limit:
        return text
    dropped = len(text) - limit
    return text[:limit] + f"\n... [truncated {dropped} bytes]"


ws = AgentWorkspace()
try:
    ws.shell("git clone --depth 1 https://example.com/repo /workspace/repo")
    ws.shell("cd /workspace/repo && pip install -e .", timeout_seconds=300)
    run_agent(ws)          # twenty-something turns against the same machine
finally:
    ws.close()             # the machine always gets thrown away

One sandbox per task is the default I would argue for. State should survive the steps of a task — that is the entire point — and it should not survive into the next one, because a file left behind from someone else's task turning up in yours is a data-leak bug dressed as a caching optimisation.

3. Agents install things constantly, and never the things you predicted

The behaviour: you pre-bake an image with the fifteen libraries you are certain the agent needs, and on the third run it decides it needs a sixteenth. Then apt. Then a Node toolchain, in a Python repo, for reasons that make sense to it. Agents install unpredictably because the repos they are handed are unpredictable, and no amount of prompting reliably stops a model from reaching for pip when a test fails on an ImportError.

The requirement: a genuinely writable filesystem with root inside the guest, and a deliberate answer for where packages come from. Read-only images with a small writable scratch directory are a good fit for serverless functions and a bad fit for agents, because the first thing an agent does when it cannot write is invent an elaborate workaround and then get stuck in it. So: writable root, plus either a template with the common runtimes already warm, or a controlled network path — a proxy or an allowlist pointing at a package mirror rather than the open internet.

Both halves matter. Preinstalled runtimes make runs fast and reproducible; the controlled path is what stops 'the agent installs what it needs' from also meaning 'the agent pulls arbitrary code from anywhere, chosen by a model, on your account.' The install step is a supply-chain surface with a language model holding the pen. Treat every unexpected install as a signal to update the template, not as normal operation.

If you find yourself writing prompt text like 'do not install packages,' you have chosen a sandbox that cannot enforce what you actually want. Prompting is not a policy engine. Network egress rules are.

4. Agents hang, loop and run away, so you need real stop buttons

The behaviour: eventually the agent runs a test suite that blocks waiting for stdin nobody will ever type. Or it writes a retry loop with no ceiling because the error message said 'try again.' Or it starts a dev server in the foreground and then waits, patiently, for a command that has already decided never to return. This is not exotic; it happens on ordinary Tuesdays.

Three distinct mechanisms are required and they are not the same thing. A per-call timeout that actually kills the remote process, rather than a client-side deadline that gives up on the HTTP request while the runaway job keeps burning CPU on the other end. A cancellation path you can hit from outside — the user pressed stop, the budget is gone, kill this now. And a TTL owned by the platform, so the sandbox reaps itself when your orchestrator crashes between creating it and cleaning it up. That third one is the one people skip, and it is the one that turns a Friday deploy into a Monday invoice for a weekend of a `while True` loop nobody was awake to see.

When you evaluate a vendor, ask specifically what a timeout does on the server side. 'The call returns an error after N seconds' and 'the process is dead after N seconds' are very different products and they are described with identical words. The mechanics, including how to plumb cancellation through an agent framework, are in /blog/agent-tool-timeouts-and-cancellation.

5. Agents produce enormous output, and you pay for all of it twice

The behaviour: `pytest -vv` on a real repo. A webpack build log. An agent that responds to a confusing failure by cat-ing the entire file, and when that does not help, cat-ing it again with line numbers. A single well-meaning command can produce more text than the rest of the conversation combined.

You pay for this twice: once in tokens, and once in quality, because burying the useful three lines of a stack trace inside forty thousand characters of noise measurably degrades what the model does next. The requirement is an output budget enforced at the sandbox layer, not hoped for at the prompt layer. Concretely: streaming, so long-running commands show progress instead of appearing hung; a hard byte cap per call; truncation that is visible to the model, because an agent that does not know it was truncated will confidently reason about output it never saw; and ideally the ability to keep full output in a file the agent can grep, so the detail exists without entering the context window.

  • Stream stdout/stderr rather than buffering to completion — a build that prints nothing for four minutes looks identical to a hang, and agents respond to apparent hangs by killing and retrying.
  • Cap bytes per call, and prefer keeping the tail over the head: the failure is almost always at the end.
  • Mark the truncation in the text you feed back, with the number of bytes dropped. Silent truncation produces confidently wrong follow-up steps.
  • Redirect big output to a file in the workspace so the agent can grep it. Full fidelity on disk, a summary in the context window.
  • Treat exit codes as the primary signal. They are one integer and they are never wrong, which cannot be said for the surrounding prose.

6. Agents act on text strangers wrote, so you need a kernel boundary

The behaviour: the agent reads the GitHub issue. It reads the README of a dependency. It fetches a page because the error message mentioned a URL. It reads source code that arrived from a package registry. Every one of those is text somebody else wrote, and the agent's next action is downstream of all of it. This is not a hypothetical about a malicious user; it is the normal operating mode of an agent doing normal work.

The requirement follows without much drama: the execution boundary must hold against code that was chosen adversarially, not merely against code that was written carelessly. In practice that means a boundary at the kernel — a microVM with its own guest kernel behind KVM, or something equivalently strong — rather than a shared kernel plus a syscall filter, which is hope with a config file attached. A container is a set of polite suggestions to a kernel your neighbours also share; that is a fine model for code you wrote and a poor one for code an attacker influenced through an issue title.

The second half is egress, and it is the half people underrate. The realistic bad outcome from prompt injection into a coding agent is not host takeover; it is exfiltration — the agent is talked into reading a credential and POSTing it somewhere. Kernel isolation does nothing about that. Default-deny outbound with an allowlist does. Ask any vendor whether egress is controllable per sandbox and what the default is; 'full internet access' as a default is a reasonable product decision and an unreasonable thing to discover after the fact. More on both halves in /blog/ai-agent-isolation-filesystem-network.

The uncomfortable version: your agent's threat model is not 'what if the model is evil.' It is 'what if the model is obedient, and it just read something written by someone who wants your keys.'

7. Agents should try two things at once, which almost no sandbox lets them

The behaviour: the agent has two plausible theories about a bug and picks one. If it picks wrong, it spends eight turns discovering that, and then has to unwind edits it has already made — badly, usually, because reverting is a skill models are worse at than editing. Meanwhile the expensive part of the environment (repo cloned, dependencies installed, database seeded, test suite warm) is identical in both worlds.

The requirement is snapshot and fork as first-class API primitives: freeze the machine mid-run, and branch it cheaply so each theory gets its own real, isolated copy. Copy-on-write is what makes this affordable — memory pages shared until a child writes one, the rootfs cloned by reflink as an O(metadata) operation — so branching costs a fraction of re-provisioning. On PandaStack a same-host fork lands in roughly 400–750ms and a cross-host fork in 1.2–3.5s. The point is not the millisecond count; it is that branching is cheaper than the setup you would otherwise repeat, which changes what patterns are worth writing.

from pandastack import Sandbox
import concurrent.futures as cf

# Pay for setup exactly once.
base = Sandbox.create(template="code-interpreter",
                      persistent=True, ttl_seconds=1800)
base.exec("git clone --depth 1 https://example.com/repo /workspace/repo")
base.exec("cd /workspace/repo && pip install -e .", timeout_seconds=300)

theories = propose_fixes(n=4)   # four candidate patches from the model


def explore(patch: str) -> dict:
    # Each branch inherits the warm repo, the venv, everything.
    child = base.fork()
    try:
        child.filesystem.write("/workspace/repo/fix.patch", patch)
        child.exec("cd /workspace/repo && git apply fix.patch")
        r = child.exec("cd /workspace/repo && pytest -q",
                       timeout_seconds=180)
        return {"patch": patch,
                "passed": r.exit_code == 0,
                "tail": r.stdout[-1200:]}
    finally:
        child.kill()        # dead ends cost a kill(), not an apology


with cf.ThreadPoolExecutor(max_workers=4) as pool:
    results = list(pool.map(explore, theories))

base.kill()
winners = [r for r in results if r["passed"]]
print(f"{len(winners)}/{len(results)} theories survived the test suite")

This is the requirement most likely to be missing from whatever you are evaluating, because it is genuinely hard to retrofit — it needs the isolation layer to support memory and disk copy-on-write, which is a substrate decision, not an API decision. Check for it explicitly rather than assuming; a 'clone this environment' endpoint that re-runs your setup script is not the same thing, and only one of them makes best-of-N cheap. The mechanics are in /blog/snapshot-and-fork-explained.

8. Agents run in fleets, so create latency and density are throughput

The behaviour: the moment your agent works at all, you start running it two hundred times — an eval suite, a nightly regression sweep, a queue of user tasks arriving in bursts. Single-session ergonomics stop being the constraint and fleet economics take over.

Three things matter here. Create latency, because it sits inside the loop and you will hit it constantly: PandaStack restores a baked Firecracker snapshot on every create at roughly 179ms p50 and 203ms p99 (the restore step itself is about 49ms), with no warm pool of idle VMs behind it; the first spawn of a template, before a snapshot exists, is a real cold boot of about 3s. Per-host density, because it decides your cost per concurrent agent — our networking pre-allocates 16,384 /30 subnets per agent host, though in practice memory, not addressing, is what caps you. And teardown that is actually clean, because orphaned sandboxes are invisible until they are an invoice, and every fleet operator has at least one story that starts with a leaked resource and ends with a spreadsheet.

For anything managed and stateful, budget separately: a managed database on our platform takes 30–90s to create, because PostgreSQL has to bootstrap and pass a readiness check, and no amount of snapshot cleverness makes that instant. That is a different latency class from a sandbox and should be provisioned outside the agent loop, not inside it.

The requirements, in one table

  • Per-second billing and near-zero idle — Why the agent forces it: sessions are long and mostly spent waiting on the model, so duty cycle is low. What to look for: per-second granularity, CPU billed on actual usage rather than allocation, and no requirement to keep an always-on instance just to hold state.
  • Persistent workspace — Why the agent forces it: step nineteen depends on the venv from step two, and the model will not rebuild it. What to look for: files, installs and processes surviving between calls for the life of a task, with a documented lifetime.
  • Live interpreter session — Why the agent forces it: code agents assume variables computed earlier still exist. What to look for: a code-context/kernel primitive with notebook semantics, distinct from a stateless run-this-snippet call.
  • Writable root filesystem — Why the agent forces it: agents install unpredictably and get stuck when writes fail. What to look for: real root in the guest, not a read-only image with a scratch directory.
  • Controlled network path — Why the agent forces it: installs are a supply-chain surface with a model holding the pen. What to look for: default-deny egress, per-sandbox allowlists, and a package mirror or proxy option.
  • Server-side timeouts and cancellation — Why the agent forces it: hung test suites and runaway loops are routine, not exotic. What to look for: timeouts that kill the remote process (not just the HTTP call), an explicit cancel API, and confirmation of which one you are getting.
  • Platform-owned TTL — Why the agent forces it: your orchestrator will crash between create and cleanup. What to look for: a TTL enforced by the platform, independent of your process, with a documented reaper.
  • Output budget — Why the agent forces it: one verbose build log costs tokens and degrades the next decision. What to look for: streaming, hard byte caps, visible truncation markers, and file-based capture for full fidelity.
  • Kernel-level isolation — Why the agent forces it: the agent acts on issue text, READMEs and pages that strangers wrote. What to look for: a guest kernel behind hardware virtualization, or an equivalently strong boundary — not a shared kernel plus a syscall filter.
  • Snapshot and fork — Why the agent forces it: exploring two theories in parallel beats unwinding the wrong one. What to look for: copy-on-write branching as an API primitive, and confirmation that it is not re-running your setup script under a nicer name.
  • Fast create and clean teardown — Why the agent forces it: evals and task queues turn one sandbox into hundreds. What to look for: create latency you can afford inside the loop, honest per-host density figures, and a leak story you find convincing.

Two deliberate omissions. Raw CPU benchmarks are near the bottom of the list, because agents are model-bound rather than compute-bound and a faster core mostly buys you a shorter wait inside a longer wait. And GPU access, unless you are building an ML agent specifically — it is the feature most likely to be sold to you and least likely to matter for a coding agent that spends its life running pytest.

The vendor checklist

Questions to ask, in the order I would ask them. Most can be answered from public docs; the ones that cannot are informative in themselves.

  1. If a sandbox is alive two hours and busy four minutes, what is the bill? Ask for the arithmetic, not the rate card.
  2. What exactly persists between calls — files only, or a live interpreter session too? Make them distinguish the two.
  3. Can the guest write anywhere and install anything, as root? If not, what breaks first?
  4. What is the default egress policy, and can I set an allowlist per sandbox without a support ticket?
  5. When a call times out, is the remote process dead, or just my HTTP request? Ask for that sentence in writing.
  6. Is there a cancel API I can hit from a different process than the one that started the work?
  7. If my orchestrator dies right now, when does this sandbox stop costing me money, and who enforces that?
  8. Is output streamed, and what happens at the byte cap — silent truncation, an error, or a marker?
  9. What is the isolation boundary, in one sentence, without marketing adjectives? Then ask whether it is shared-kernel.
  10. Can I snapshot a running sandbox and fork it, and is the fork copy-on-write or a re-provision?
  11. What is the create latency for my template, in my region, measured cold — and can I reproduce it myself?
  12. How do orphaned sandboxes get reaped, and can I list every sandbox my account currently owns in one call?
  13. Can I self-host if the pricing or the roadmap moves somewhere I do not want to go?
Benchmark the ones that matter to you rather than trusting any published figure, ours included. Cold-start in particular is the easiest metric in this category to measure differently and report honestly — warm pool versus true cold boot, snapshot restore versus full boot, their region versus yours. Treat every headline number as a hypothesis.

The bottom line

Derive the requirements from the agent and the list stops looking like a spec sheet. It becomes: bill me only when work happens, remember what I did, let me install things, let me stop things, do not drown me in output, hold the line against text strangers wrote, let me try two ideas at once, and do all of it a few hundred times without leaking. That is an unglamorous list. It is also the list that decides whether your agent survives a week of real repositories and real users.

PandaStack is our answer to it — Apache-2.0 Firecracker microVMs you can self-host, snapshot-restore on every create at about 179ms p50, copy-on-write fork as a first-class primitive, and per-second billing so the waiting is nearly free. It will not be the right answer for everyone, and there are good tools on the other side of several of these trade-offs; the survey is in /blog/best-sandboxes-for-ai-coding-agents-2026. But run whichever candidate you are considering through the checklist above rather than through its feature page. The feature page was written for a buyer. The checklist was written for the agent, and the agent is the one that has to live there.

Frequently asked questions

What is the single most important thing a coding agent needs from a sandbox?

Persistent state across steps, closely followed by an isolation boundary that holds against adversarial input. Agents accumulate a working environment over dozens of turns — a cloned repo, an installed virtualenv, edited files, computed variables — and at step nineteen they simply assume all of it is still there. A stateless run-this-snippet API forces the agent to rebuild its world every turn, which burns tokens and produces worse plans. Everything else on the list is real, but if state does not survive, the agent does not work at all rather than working expensively.

Why does idle cost matter more than performance for agent sandboxes?

Because agents are model-bound, not compute-bound. Over a long session the sandbox spends most of its wall-clock time waiting for an LLM to produce the next action, and only a small fraction actually executing anything. That makes billing granularity and idle behaviour more consequential to your bill than raw CPU throughput: a faster core shortens a small wait inside a much longer one, whereas hourly billing charges you for the entire wait. Look for per-second billing and CPU charged on actual usage. On PandaStack that is $0.054 per active vCPU-hour and $0.0162 per GiB-hour, metered per second — but the shape matters more than the specific rate, whoever you buy from.

Is a container good enough for a coding agent?

It depends entirely on whether the agent reads text you do not control, and in practice almost all coding agents do — issue descriptions, READMEs, web pages fetched from an error message, dependency source code. Once the agent's actions are downstream of attacker-influenceable text, you are running adversarially-chosen code, and a shared host kernel plus a syscall filter is a weaker boundary than that deserves. A microVM with its own guest kernel behind hardware virtualization contains that class of failure to one disposable machine. Note also that kernel isolation alone does not stop the likeliest bad outcome, which is exfiltration — you need default-deny network egress alongside it.

Why do so few sandbox APIs support snapshot and fork?

Because it is a substrate decision rather than an API decision. Cheap branching requires copy-on-write at both layers — guest memory shared until a child writes a page, and the root filesystem cloned as an O(metadata) reflink rather than copied — and that has to be designed into the isolation layer from the start. It is very hard to retrofit onto a platform that was not built for it, which is why many providers instead offer a clone endpoint that re-runs your setup script. That is a useful convenience but a completely different cost profile. If best-of-N or parallel exploration is central to your agent, verify which one you are getting: on PandaStack a same-host copy-on-write fork lands in roughly 400–750ms and a cross-host fork in 1.2–3.5s, because the setup work is inherited rather than repeated.

How do I stop an agent's runaway loop from costing me a fortune?

Three independent mechanisms, because each one covers a failure the others do not. A per-call timeout that kills the remote process rather than merely abandoning your HTTP request — ask a vendor to state explicitly which of those two their timeout does, because the wording is usually identical. An out-of-band cancellation API so a supervisor, a budget guard or a user pressing stop can terminate work started by a different process. And a TTL enforced by the platform itself, which is the only one that saves you when your orchestrator crashes between creating a sandbox and cleaning it up. That third mechanism is the one most often skipped and the one that turns an unnoticed loop into a weekend of billed compute.

Keep reading

Run code in a microVM in one API call.

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

Start free
Written by Ajay Kumar, Founder, PandaStack.