Skip to content
agent.run()Go to AgentRun
← All posts
Engineering·

Squeezing the juice out of our sandboxes

At Parcha we started building agents in 2023, before sandboxes were a thing.

At Parcha we started building agents in 2023, before sandboxes were a thing. MicroVMs existed, but an agent was mostly a long-running process that called LLMs and third-party providers. It didn't run much untrusted code, and the biggest risk was prompt injection. Isolating agents in sandboxes didn't matter much yet, because those agents weren't really agents. They were workflows: tightly controlled loops of LLM calls, tool calls and answers.

Then today's agents showed up, starting with the Claude Code SDK, later renamed the Claude Agent SDK. I adopted it incrementally, as I wrote in Claude in a Box. That post reads like ancient history now, but it's less than a year old. I wrapped the SDK, added utilities to load skills and tools, and kept running the agents on the same Celery workers that ran Parcha's controlled workflows. I locked them down as much as I could: every tool off by default, no Bash, no code. It was clear early on that this wasn't the way to go.

So I found sandboxes, first Modal and then @daytonaio. Again I went incremental: I removed the runner and made the sandbox the runner too. The whole agent moved inside the sandbox, loop and tools together. I controlled egress, made sure no secrets or data I cared about could leak, put a bunch of controls around it and called it a day. There was one more piece: with the loop inside the sandbox, the agent's state and every status the user sees lived in there too. So I built message passing to get them out to the control plane. The sandbox published statuses and the agent's session to a separate Redis, and a set of job manager replicas consumed them, saved them to Postgres and pushed them to the user. That worked well for a while, at a scale that admittedly wasn't huge, so I didn't care much how well I used the sandbox's resources. People kept talking about separating the brain from the hands, but my model was simple enough that it just worked.

Then growth happened, and swarms. Running one agent at a time is different from one click that runs 1,000 or 10,000, and some Parcha customers now run hundreds of thousands to clear backlogs or analyze many entities at once. That needs scale on many levels. Here I'll focus on one: sandboxes.

Brains and hands

In AgentRun I finally separated the brain from the hands. There are two ways to run the loop:

  1. The agent lives in the sandbox. The loop runs there, every tool call runs on the same machine, and its state and statuses have to be sent out as messages.
  2. Brains in a runner, hands in a sandbox. The loop runs in a runner. Anything untrusted, like writing and running code, runs in a sandbox, which hands its output back to the loop.
Architecture comparison of one sandbox per agent, a runner with a shared sandbox pool, and a runner using Archil execution.
Architecture comparison of one sandbox per agent, a runner with a shared sandbox pool, and a runner using Archil execution.

What changed my mind

I was stuck on simplicity. Why have two kinds of machines, one hosting the loop and one running the tools, when you can have one? One seems easier than two.

What I hadn't considered, until I looked at runs with hundreds, thousands and tens of thousands of agents, is how wasteful it is to give each agent its own sandbox for its whole run. You do it so data can't leak between customers, or just to keep things simple. But agents are notoriously I/O-bound. They spend almost all their time waiting on an LLM, a tool call or some other operation, and do very little computing themselves. In my runs, one turn of the loop used about 14 ms of CPU out of about 3 seconds. A sandbox dedicated to one agent spent only 7–8% of its billed time doing any work, and 3% with a reasoning model.

Separate the two and the system gets simpler in a different way.

How it works in AgentRun

My open-source setup is simple: a Node runner runs the loops, and a sandbox runs the tool calls. I'm strongly in favor of the agent being its workspace. In my case that's an @archildata disk holding everything the agent works with: the files it reads, changes and produces. That simplifies a lot, because the runner, the sandbox and the control plane can all reach the same disk.

With that in place, an agent runs like this:

  1. The loop starts in a runner.
  2. When the agent needs to look at a file the user gave it, or load a skill, the runner reads it through Archil's S3 API. Nothing gets mounted in the runner. With many customers' loops in one runner, mounting disks there would be asking for data leaks.
  3. When the agent makes a tool call that writes or runs code, or anything else I don't trust, it takes a sandbox from a pool, mounts the agent's folder, runs the tool and releases the sandbox.

That sandbox was busy for this agent for well under a second (a median of about 0.6–0.7 s per call in my runs), then went back to the pool for the next agent. A pool of disposable compute that holds no state, plus Archil to attach state on demand, is what makes this efficient.

How efficient?

I ran both models on the same agents and the same plans at 100, 1,000 and 10,000 agents. Every agent ran 24 turns, mixing web searches with tool calls that wrote and analyzed data files in its sandbox. One Node process held about 300 agent loops before runs started to slow down. Compare that with one agent per sandbox. 10,000 agents ran on 4 Node processes, each using about half a gigabyte of memory and well under one core on average. The same 10,000 agents' 127,653 tool calls were served by a pool of 120 sandboxes. This was a quick experiment: I didn't tune the runner, and I didn't test loops that do a lot of their own I/O. At the 10,000-agent peak, runs took about 20% longer than with 100 agents, so a fifth process would have helped.

Recorded agent and sandbox replays Open full size · Download HTML
Static versionVideo poster of the 10,000-agent sandbox-pool demonstration, displaying 120 sandboxes and execution counters.

10,000 agents, both models. The agent-per-sandbox side is projected from recorded runs.

Here is where the sandbox time goes, with the same 1,301 tool calls in each model:

Dollars per 1,000 agent runs100 agents1,000 agents10,000 agentsReasoning model, 100 agents
Agent in its own sandbox$1.39$1.41$1.41 (estimated)$4.71
Runner + shared sandbox pool$0.53$0.51$0.40$0.97
Runner + Archil exec$0.67$0.64$0.65$0.67

The gap grows with how long the agent thinks, because its own sandbox bills the whole time.

What pi-durable let me delete

Remember the message passing from the sandbox days? It existed because the loop lived in the sandbox, so everything the control plane needed (the agent's session, its progress, the statuses the user sees) had to be sent out of it.

With the loop in my runner, that problem goes away. The runner is my own process, and pi-durable commits the agent's state to a SQLite database at every step. I keep that database in the agent's folder on its Archil disk, and the control plane reads the same file to show the user where the run is. A sandbox only gets the agent's folder mounted for one tool call and hands the output back to the runner. No sandbox Redis, no stream consumers, no session relay.

Before-and-after diagrams compare an agent loop inside a sandbox with a durable loop running in a separate runner.
Before-and-after diagrams compare an agent loop inside a sandbox with a durable loop running in a separate runner.

Can I do even better?

Even with a pool, I still run sandboxes. I size the pool for the peak, keep it warm, replace the ones that break, and for every tool call I claim a sandbox, mount the agent's folder, run the command, unmount and release it. At 10,000 agents that was 120 sandboxes and 127,653 of those cycles.

This year Archil, which already holds the agents' state, launched serverless execution and then persistent sandboxes. With serverless execution you don't run sandboxes at all. You call exec with a command; Archil boots a sandbox next to the disk, mounts the agent's folder, runs the command, returns the output and throws the sandbox away. The runner makes one call per tool call, and everything else on the pool's list stops being mine. That's what I'm evaluating now.

Diagrams compare borrowing a sandbox from a shared pool with executing a function directly beside an Archil disk.
Diagrams compare borrowing a sandbox from a shared pool with executing a function directly beside an Archil disk.

I ran the same agents through it. Boot and mount took about 40–65 ms, and a tool call took a median of 0.33–0.37 s end to end, about half the pool's time, partly because exec's default sandbox is twice the size of my pool's. It bills per call ($0.05 per 1,000), so what an agent costs depends on how many tool calls it makes, not on how long it thinks: $0.65 per 1,000 runs at 100, 1,000 and 10,000 agents, and $0.67 with a reasoning model. A pool that stays busy is still cheaper ($0.40 per 1,000 runs at 10,000 agents). What exec buys is less infrastructure and one less vendor. The tradeoff is trusting Archil with more.

Persistent sandboxes cover the tools that need more than one command, like a service that has to keep running: files and installed packages survive a stop and start, and a sandbox can pause and resume.

Beyond sandboxes

Another argument for giving each agent its own computer is keeping state beyond the files in a workspace. A browser is the classic example: one that stays logged in to your favorite sites on a Mac Mini or a cloud machine, so the agent doesn't have to log in every time or keep credentials somewhere unsafe. You don't need that anymore. Cloud browser vendors like @browserbase now save a browser's context (cookies, logins, local storage) and attach it to a fresh browser when you ask for one. Same idea: separate the state from the compute. The browser is compute; the context you attach to it is state. You get your logged-in browsers and profiles without keeping a computer running all the time.

The same goes for agents that need a whole computer. Vercel's sandboxes save their filesystem when they stop and pick up where they left off on the next call. Archil and E2B can pause a sandbox with its memory and running processes and resume it later. Your agent's loop runs somewhere else, and you start that computer only when a tool needs it.

Where this goes next

I think this is where the market is going. Agent state is moving onto disks and into durable logs, and compute is moving to wherever that state lives, called per command instead of rented per agent. A sandbox becomes something a tool call uses for a few hundred milliseconds, not the place an agent lives.

DeepSeek recently published its take on super agents. In their design an agent is always online: a long-lived router watches events, stays quiet when nothing needs doing, and hands each task to a fresh worker instead of waiting for a person to start a run. That only works if a loop that sits idle almost all the time costs almost nothing while it idles. With pi-durable keeping the loop's state and Archil keeping its files, the loop can run wherever it's cheapest, whether that's a runner, a browser through WebAssembly, a GPU cluster or a phone, and reach the compute it needs only when it needs it. You pay only for what you use.

Miguel Rios Berrios

Miguel Rios Berrios is the founder of Parcha Labs, the makers of GREP.ai and AgentRun.ai. He was Senior Director of Engineering at Brex, leading the Platform Engineering organization, and Head of Data Science at Twitter.