all posts

Giving Every Agent in a Swarm Its Own Sandbox

Ajay Kumar··8 min read

Multi-agent frameworks all sell the same dream: don't run one agent, run a swarm. LangGraph fans out a supervisor into workers, CrewAI gives each crew member a role, AutoGen has agents argue in a group chat, and Claude sub-agents spin off children that each go do a piece of the job. It's a good dream — parallel exploration, division of labor, best-of-N on real work. Then you get to the part nobody puts on the landing page: every one of those agents wants to write code and run it, and if they all run it in the same place, they are twenty roommates sharing one kitchen with no labels on anything. Agent 3 writes `/app/main.py`; agent 7 overwrites `/app/main.py`; both bind port 8000; agent 11 decides the workspace is dirty and runs `rm -rf ./*` to start fresh, and now the swarm is a crater. This post is about the boring fix that makes the dream actually work: one sandbox per agent, cheap enough that fanning out twenty of them costs about as much as booting one.

The problem: a swarm in one sandbox is a swarm fighting itself

The instinct, when you're wiring up a multi-agent system, is to provision one execution environment and let every agent share it. It's simple, it's one thing to manage, and for a single agent it's exactly right. The moment there are N agents writing and running code concurrently in that one environment, you've built a distributed system with no isolation — which is to say, a pile of race conditions wearing a trench coat. The failures aren't exotic; they're the dullest possible bugs, which is what makes them so maddening to debug across a non-deterministic swarm.

  • They clobber each other's files. Two agents both scaffold a project at /workspace, both write config.json, both `git checkout` a different branch of the same repo. Whoever ran last wins, and the other agent is now reasoning about a filesystem that silently changed underneath it.
  • They race on ports and processes. Agent A starts a dev server on :3000 to test its work; agent B tries the same port and gets EADDRINUSE, or worse, connects to A's server and 'tests' A's code thinking it's testing its own. Background processes from one agent leak into another's `ps` output and its debugging.
  • They poison shared mutable state. One agent `pip install`s a pinned dependency version, breaking another agent that needed a different one. One sets an environment variable, exports a shell function, or edits ~/.bashrc, and the blast radius is every other agent in the box.
  • One agent's mistake is everyone's mistake. The genuinely dangerous one: an agent that decides to 'clean up' or 'reset the environment' runs a destructive command — `rm -rf`, `git clean -fdx`, `DROP TABLE` — and there is no wall between its regret and the other nineteen agents' work. A single confused agent takes down the swarm.
This is a correctness problem before it's ever a security problem. Even if every agent in your swarm is perfectly trusted and well-meaning, sharing one mutable environment across N concurrent code-runners means their side effects collide. Isolation here isn't about defending against attackers — it's about the swarm producing correct results at all.

Why per-agent isolation is about correctness, not just security

Most sandboxing arguments are security arguments: the code is untrusted, so put a wall around it. That argument applies to swarms too — an agent running model-written code is running code no human reviewed — but it's not the interesting part here. The interesting part is that per-agent isolation is what makes parallel agent work mean anything. If agent 1 and agent 2 are supposed to explore two different approaches to the same problem, they have to explore them in two different worlds. Share a world and you don't have two experiments, you have one contaminated experiment where each agent's changes leak into the other's results.

Give each agent its own machine and the swarm gets three properties it can't have otherwise. First, parallel exploration is real: agent 1 rewrites the module one way, agent 2 rewrites it another, and neither sees the other's edits, so when you compare their test results you're comparing the approaches, not an accident of ordering. Second, there's no shared mutable state to poison: each agent installs its own deps, binds its own ports, and pollutes its own environment, and the pollution stays home. Third, failures are contained by construction: an agent that corrupts its filesystem, wedges its interpreter, or runs the catastrophic `rm -rf` does all of it inside a VM that belongs to nobody else. You lose one agent's work, not the swarm's.

Two agents exploring two approaches in one sandbox aren't running two experiments — they're running one experiment with a contamination bug. The wall between them isn't paranoia; it's the whole point of running them separately.

The pattern: one microVM per agent, created on demand

The fix is exactly as simple as it sounds: when you fan out N agents, create N sandboxes, hand each agent its own, and tear them down when the work is collected. The reason this pattern is usually rejected — and the reason people reach for the doomed shared sandbox instead — is a cost intuition that's simply wrong for microVMs: 'a VM per agent is too slow and too expensive.' That's true for cold-booting conventional VMs. It's false for a snapshot restore, which is a fundamentally different operation.

A cold boot does real work: initialize a kernel, bring up userspace, load a runtime — tens of seconds, and you'd feel every one of them multiplied by twenty. A snapshot restore skips all of it: PandaStack maps a frozen, already-booted VM's memory and device state back in and resumes it mid-instruction, which lands at a p50 of about 179ms (p99 around 203ms; the restore-and-resume core is roughly 49ms). The genuine ~3-second cold boot happens exactly once per template, gets baked into a snapshot, and is amortized away forever after. So the create-per-agent loop is: for each agent, restore a fresh sandbox from the baked template snapshot, and because those restores run concurrently, fanning out twenty agents costs about 179ms of wall-clock, not twenty times a boot.

The key mental shift: creating a sandbox per agent is a parallel operation, not a serial one. You don't pay N × boot_time — you fire off N restores at once and pay roughly one restore's latency (~179ms) plus scheduling. The swarm's size stops being a cost variable for provisioning.

Fanning out a swarm in Python

Here's the whole pattern with the Python SDK: take a list of agent tasks, create one microVM per task concurrently, run each agent's task in its own sandbox, gather the results back, and delete every sandbox. Set PANDASTACK_API_KEY in the environment first (install with pip install pandastack). The `metadata` tag stamps each VM with its agent id and the swarm's run id, so if something goes wrong you can attribute a runaway VM to exactly one agent, and `ttl_seconds` is a backstop so a hung agent's VM reaps itself instead of renting a room forever.

import concurrent.futures as cf
import uuid
from dataclasses import dataclass
from pandastack import PandaStack

ps = PandaStack()  # reads PANDASTACK_API_KEY, base url https://api.pandastack.ai

@dataclass
class AgentTask:
    agent_id: str        # e.g. "worker-3"
    prompt: str          # what this agent should do
    code: str            # the code this agent wrote for its task

def run_agent(task: AgentTask, run_id: str) -> dict:
    # 1. Fresh, isolated microVM for THIS agent only. Every create restores a
    #    baked snapshot (~179ms p50), so one-VM-per-agent is cheap. The metadata
    #    ties the VM to a single agent + swarm run for attribution and cleanup.
    sb = ps.sandboxes.create(
        template="code-interpreter",
        ttl_seconds=600,                      # backstop: a hung agent reaps itself
        metadata={"run": run_id, "agent": task.agent_id, "kind": "swarm-worker"},
    )
    try:
        # 2. This agent owns its whole filesystem, ports, and process table.
        #    Nothing it writes here can touch a sibling agent's world.
        sb.filesystem.write("/work/task.py", task.code)

        # 3. Run the agent's task. Another agent binding :8000 or running
        #    `rm -rf /work` next door is physically incapable of reaching this VM.
        result = sb.exec("cd /work && python3 task.py", timeout_seconds=120)

        return {
            "agent": task.agent_id,
            "ok": result.exit_code == 0,
            "stdout": result.stdout[-4000:],
            "stderr": result.stderr[-2000:],
            "sandbox": sb.id,
        }
    finally:
        # 5. Tear down this agent's VM. Its files, ports, and pollution vanish
        #    with it. Idle cost of the swarm after this line is ~zero.
        sb.delete()

def run_swarm(tasks: list[AgentTask]) -> list[dict]:
    run_id = uuid.uuid4().hex[:8]
    # 4. Fan out: N agents, N sandboxes, all created + run concurrently. The
    #    provisioning cost is ~one restore of wall-clock, not N boots in series.
    with cf.ThreadPoolExecutor(max_workers=len(tasks)) as pool:
        futures = [pool.submit(run_agent, t, run_id) for t in tasks]
        return [f.result() for f in cf.as_completed(futures)]

# Collect results back into the supervisor and decide what to keep.
results = run_swarm(swarm_tasks)
winners = [r for r in results if r["ok"]]
print(f"{len(winners)}/{len(results)} agents succeeded")

The shape is always the same: create per agent, run in isolation, gather, delete. Note the two safety rails, because they aren't decoration. The `timeout_seconds` on `exec` is a circuit breaker for an agent whose code loops forever; the `ttl_seconds` on create is a backstop so a VM you forget to reap reaps itself. And because each `sb.delete()` returns the VM's compute, the swarm's cost after collection drops to roughly zero — you're not paying to keep twenty idle rooms warm between runs.

Best-of-N: fork the whole swarm from one warm base

Create-per-agent gives each agent a clean, generic template. Often that's exactly what you want — twenty agents each starting from a blank code-interpreter. But sometimes every agent in the swarm needs the same expensive setup first: the same repo cloned, the same dependencies installed, the same dataset loaded. Doing that setup twenty times is twenty times the wasted minutes. This is where forking earns its keep.

Warm one base sandbox — clone the repo, run the install, load the data — then fork it once per agent. A same-host fork on PandaStack lands in roughly 400–750ms (cross-host 1.2–3.5s) and moves no bulk data: it reflinks the rootfs and maps the parent's memory copy-on-write, so each child inherits the entire warm environment for free and only allocates storage for the bytes it changes. Now your swarm of twenty agents shares one cloned repo and one installed dependency tree, each agent gets an isolated machine to diverge in, and you've paid the expensive setup exactly once instead of twenty times. It's the best-of-N pattern: warm once, fork per candidate, run all branches in parallel, keep the winners, delete the rest — and the losing branches barely diverged, so discarding them reclaims almost nothing.

Forking clones the machine, not the outside world. If several forked agents all send an email, charge a card, or write to a shared external database, every one of them does it — you'll fire twenty emails and keep one result. Score agents on in-VM signals (tests, builds, local output) and make external side effects the job of the winner only, after you've picked it.

Collecting results and cleaning up

Isolation makes the fan-out safe; the fan-in is where you turn twenty isolated results back into one answer. Because each agent's work lived in its own VM, collecting it is deliberate: you read exactly what you want out of each sandbox — a result file, stdout, an exit code, a produced artifact — before you tear it down. That extraction is the only thing that escapes the VM, which is precisely the property you want. Nothing leaks between agents by accident; things cross the boundary only because you reached in and pulled them out.

Cleanup deserves the same discipline as provisioning. A swarm that creates cleanly but reaps sloppily just relocates the resource leak from the filesystem to your bill. Three habits keep it honest:

  • Delete in a finally block. Every agent's VM should be torn down whether its task succeeded, failed, or threw — put the `sb.delete()` in a finally so an exception in one agent's task doesn't strand its VM.
  • Use TTL as a backstop, not the primary path. `ttl_seconds` guarantees a forgotten or hung VM self-destructs, but rely on explicit deletes for the common case so you're not paying for the whole TTL window on every run.
  • Tag everything with a run id. The `metadata` run id means you can list and force-delete every VM from a given swarm run in one sweep if your supervisor crashed mid-fan-out — attribution is what makes recovery a query instead of a hunt.

VM-per-agent vs. shared sandbox vs. container-per-agent

There are three ways to give a swarm somewhere to run code, and it's worth laying them side by side rather than defaulting to whichever your framework's quickstart happened to show:

  • Isolation — VM-per-agent (microVM): strongest; each agent gets its own guest kernel behind a hardware virtualization boundary, so filesystems, ports, processes, and crashes are fully separated. Shared sandbox: none between agents; every agent shares one filesystem, one process table, one set of ports. Container-per-agent: partial; separate namespaces and cgroups give each agent its own filesystem and PID space, but they all share the host's single kernel.
  • Correctness under concurrency — VM-per-agent: safe by construction; no shared mutable state means no cross-agent collisions, so parallel exploration is genuinely parallel. Shared sandbox: unsafe; concurrent code-runners clobber files, race ports, and poison shared deps — the failure mode this whole post is about. Container-per-agent: mostly safe for filesystem and ports; still shares host-kernel state (some sysctls, the kernel page cache, kernel bugs) and one agent can exhaust host kernel resources for the others.
  • Blast radius of a bad command — VM-per-agent: one agent; a destructive `rm -rf` or fork bomb is trapped in that agent's VM. Shared sandbox: the entire swarm; one agent's mistake destroys everyone's work. Container-per-agent: usually one container, but a container escape (a real, recurring vulnerability class) reaches the host and every sibling.
  • Provisioning cost for N agents — VM-per-agent: ~179ms per restore, done concurrently, so ~one restore of wall-clock for the whole fan-out; idle cost after teardown is ~zero. Shared sandbox: cheapest to provision (one environment) but you pay the savings back in correctness bugs. Container-per-agent: fast to start and cheap, the main reason people reach for it — the cost you're not seeing is the shared-kernel boundary.
  • When to use it — VM-per-agent: any swarm running model-written code where correctness and containment matter, i.e. most of them. Shared sandbox: a single agent, or a swarm of fully trusted agents doing genuinely non-overlapping, read-only work. Container-per-agent: trusted first-party agents where you want isolation but have decided a shared kernel is an acceptable boundary — never the right call for arbitrary model-written code you can't review.

The honest summary: a shared sandbox is fine for exactly one agent and a trap for a swarm; a container-per-agent buys you filesystem and port isolation but leaves a shared kernel under the whole fleet; and a microVM-per-agent is the only option that separates agents at every level that a concurrent swarm actually collides on — and snapshot-restore is what makes it cost about the same as the weaker options.

When one sandbox for the whole swarm is fine

Per-agent isolation is the default you should reach for, but it isn't free complexity to sprinkle everywhere. A shared environment is genuinely fine when the agents don't actually collide. If your 'swarm' is really one planning agent that occasionally calls a tool, there's nothing to isolate — one sandbox is correct and simpler. If your agents only ever read from a shared, immutable dataset and write their outputs to separate destinations (different S3 keys, different DB rows), they don't have shared mutable state to poison, so co-tenancy is safe. And if the agents run strictly sequentially — a pipeline where agent 2 starts only after agent 1 finishes and hands off — then there's no concurrency to race, and reusing one sandbox across the stages is reasonable.

Reach for one microVM per agent the moment agents run concurrently and each writes code or mutates state: parallel exploration where you'll compare results, best-of-N where you want the strongest of several attempts, or any swarm where a single agent's destructive command must not be able to reach its siblings. In those cases — which is most real multi-agent work — snapshot-restore gives you a genuine per-agent machine at container-like speed, so the correct architecture and the cheap architecture are finally the same one. Give every agent its own room. The one who runs `rm -rf` will thank you, and so will the other nineteen.

Frequently asked questions

Why can't a multi-agent swarm just share one sandbox?

Because N agents writing and running code concurrently in one environment collide on shared mutable state. They overwrite each other's files, race on the same ports, and poison shared dependencies and environment variables — and a single agent's destructive command like rm -rf can wipe out the whole swarm's work. This is a correctness problem before it's a security one: even fully trusted agents produce wrong results when their side effects leak into each other. A shared sandbox is correct for exactly one agent; for a concurrent swarm it turns parallel work into a pile of race conditions.

Doesn't creating one microVM per agent make fan-out too slow or expensive?

No, because each per-agent sandbox is a snapshot restore, not a cold boot. PandaStack restores a baked template snapshot in about 179ms at p50 (203ms p99), and because you fire the restores off concurrently, fanning out twenty agents costs roughly one restore of wall-clock — not twenty boots in series. The genuine ~3-second cold boot happens once per template and is amortized away. Idle cost after you delete the VMs is near zero, so the swarm isn't renting empty rooms between runs. Creating a sandbox per agent stops being a cost variable and becomes the cheap default.

How do I give every agent in a LangGraph, CrewAI, or AutoGen swarm its own sandbox?

Regardless of framework, the pattern is the same: when the graph or crew fans out into N agents, create N sandboxes concurrently, hand each agent its own for the code it runs, gather each agent's results (a file, stdout, an exit code) back into the supervisor, then delete every sandbox in a finally block. Tag each VM with the agent id and a swarm run id in metadata so you can attribute and force-clean any stragglers. With PandaStack the create is a ~179ms restore, so this fan-out adds almost no latency over sharing one environment while giving each agent a fully isolated machine.

When should agents in a swarm fork from a shared base instead of each creating fresh?

Fork when every agent needs the same expensive setup first — the same repo cloned, the same dependencies installed, the same dataset loaded. Warm one base sandbox with that setup, then fork it once per agent. A same-host fork on PandaStack lands in roughly 400–750ms and moves no bulk data (copy-on-write memory plus a reflinked rootfs), so each agent inherits the whole warm environment for free and only pays for what it changes. Use plain per-agent creates instead when each agent should start from a generic clean template — a 179ms create is simpler than a fork and gives a guaranteed-clean baseline.

Isn't a container per agent good enough for isolating a swarm?

A container per agent does give each agent its own filesystem, PID space, and ports, which fixes most of the collision problems — so it's better than a shared sandbox. Its limit is the boundary: all the containers share the host's single kernel, so they share kernel-level state and a container escape (a recurring vulnerability class) reaches the host and every sibling agent. For trusted first-party agents that may be an acceptable trade. For arbitrary model-written code you can't review, a microVM gives each agent its own guest kernel behind a hardware virtualization boundary — and at ~179ms per restore it costs about the same to stand up as a container, so there's little reason to accept the weaker wall.

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.