# Should Each Agent Tool Call Get a Fresh Sandbox?

> Use a fresh sandbox per call when the input is untrusted or calls must not see each other. Start it from a pre-warmed named snapshot: on prod that took 3.2 seconds per call versus 12.9 seconds installing dependencies each time. Reuse one sandbox per session when calls build on each other: 0.55 seconds per call.

Source: https://sandboxer.com/guides/a-fresh-sandbox-per-tool-call. Updated 2026-10-27.

**Short answer:** Use a fresh sandbox per call when the input is untrusted or calls must not see each other. Start it from a pre-warmed named snapshot: on prod that took 3.2 seconds per call versus 12.9 seconds installing dependencies each time. Reuse one sandbox per session when calls build on each other: 0.55 seconds per call.

## The three patterns

| Pattern | Per call | Once per session or deploy |
| - | - | - |
| (a) Fresh sandbox per call | create, install, run, delete | nothing |
| (b) One sandbox per session | run | create, install; delete at the end |
| (c) Fresh sandbox per call from a warm template | create `--from` the template, run, delete | install once, `sbx snapshot <id> <name>` |

(a) and (c) give every call a clean VM. (b) shares one VM, its files, its processes and its installed packages across all of a session's calls.

## Measured on prod

We ran each pattern 5 times in a row on 2026-10-27 between 09:56 and 09:58 UTC, one sandbox at a time, all `small` (2 vCPU, 4 GB). The tool call was the same each time: a Python one-liner that needs `sympy`, which isn't in the base image.

```bash
~/v/bin/python -c 'import sympy; print(sympy.factorint(2**64 - 1))'
```

```text
{3: 1, 5: 1, 17: 1, 257: 1, 641: 1, 65537: 1, 6700417: 1}
```

Times are wall clock around each Python SDK call, measured from our machine outside Sandboxer's network, so they include every HTTP round trip.

| Pattern | Call 1 | Call 2 | Call 3 | Call 4 | Call 5 | Median |
| - | - | - | - | - | - | - |
| (a) fresh, install each time | 14.17 s | 12.85 s | 12.76 s | 12.94 s | 14.91 s | **12.94 s** |
| (b) reused session sandbox | 0.61 s | 0.51 s | 0.59 s | 0.55 s | 0.55 s | **0.55 s** |
| (c) fresh, from warm template | 3.18 s | 3.11 s | 3.21 s | 3.34 s | 3.03 s | **3.18 s** |

Where the time went:

| Step | (a) | (b) | (c) |
| - | - | - | - |
| Create to ready | 2.55 s (4.27 s on call 1) | 2.62 s, once | 2.55 s |
| `venv` + `pip install sympy` | 9.36 to 11.83 s | 10.19 s, once | 0 (already in the template) |
| Run the tool | 0.50 to 0.56 s | 0.51 to 0.61 s | 0.45 to 0.75 s |
| Delete call returns | 30 ms | 30 ms, once | 30 ms |
| One-time template save | | | 2.09 s (`sandbox.snapshot(name)`) |

Three things stand out:

- **The install, not the VM, is the slow part of (a).** Create to ready was 2.55 seconds on 9 of 10 fresh creates. The `pip install` was 9 to 12 seconds every time.
- **A template removes the install, not the boot.** (c) still pays the 2.55-second create, so it is about 2.6 seconds slower per call than reuse.
- **Delete is cheap to wait for.** The delete call returned in 30 ms; the VM is torn down in the background.

For how create, resume and fork times vary over more runs, see [How fast does a cloud sandbox start, resume and fork](/guides/how-fast-does-a-cloud-sandbox-start-resume-and-fork).

## What each call costs

Billing is per second from when a sandbox is up until it stops or is deleted. `small` is $0.018/hour, which is $0.000005 per second. The numbers below charge each call for its full wall time, so they're an upper bound: the meter doesn't start until the VM is up.

| Pattern | Billed time per call (at most) | Per call | Per 1,000 calls |
| - | - | - | - |
| (a) fresh, install each time | 13.5 s (mean of 5) | $0.000068 | $0.068 |
| (c) fresh, from warm template | 3.2 s (mean of 5) | $0.000016 | $0.016 |
| (b) reused session sandbox | The whole session, not the call | Depends on the gaps | Depends on the gaps |

(b) is the one to watch. A session sandbox bills while the model is thinking between calls, not only while a command runs. With 10 seconds of model time between calls, each call costs about 10.6 seconds of `small` time ($0.000053), which is more than (c). Once the gap between calls is longer than about 2.6 seconds, a template-backed fresh sandbox per call costs less than holding one open. Always give a session sandbox a TTL or an idle pause so a crashed agent doesn't keep billing.

A `default` sandbox costs twice these numbers. The template also takes storage while it exists; storage isn't billed, but it counts toward your plan's included amount. See [Billing](/docs/billing).

## When isolation per call is worth it

Pick a fresh sandbox per call, (a) or (c), when any of these is true:

- **The input is untrusted.** Code written by a model from a user's prompt, a file a user uploaded, or a repository you didn't write. A fresh `noEnv` sandbox holds none of your secrets, and its `SANDBOXER_TOKEN` can only read that sandbox and host its ports. Whatever the code does is gone when you delete it.
- **Calls belong to different users or tenants.** One call must never read another's files or see its processes.
- **You need reproducible results.** Evals, graders and tests that must start from the same disk every time.
- **A call might wreck the machine.** `rm -rf`, a fork bomb or a full disk only takes down that call's VM.

Pick one sandbox per session, (b), when:

- **Calls build on each other.** The agent writes a file in one call and runs it in the next, or keeps a dev server or REPL running.
- **The inputs are yours or already trusted.** Your own agent working on your own repo.
- **Latency matters more than cleanliness.** 0.55 seconds per call against 3.2.

Mixed is common: a session sandbox for the agent's own work, plus a fresh `noEnv` sandbox for the one tool that runs user-supplied code.

## The code

This is the shape we used, with the Python SDK (`pip install sandboxer-sdk`). We ran this exact file on prod once per pattern after the benchmark.

```python
from sandboxer import Sandboxer

client = Sandboxer()  # reads SANDBOXER_API_KEY
SETUP = "python3 -m venv ~/v && ~/v/bin/pip install -q sympy"
TEMPLATE = "tool-call-template"


def run_fresh(code: str) -> str:
    """(a) A new sandbox for this one call, deleted afterwards."""
    sb = client.create(type="small", ttl_seconds=600, no_env=True,
                       name="tool-call-a")
    try:
        sb.exec(SETUP, timeout_seconds=300, check=True)
        return sb.exec(["/home/user/v/bin/python", "-c", code], check=True).stdout
    finally:
        sb.delete()


class Session:
    """(b) One sandbox for the whole agent session, reused by every call."""

    def __init__(self):
        self.sb = client.create(type="small", ttl_seconds=1800,
                                name="tool-call-b")
        self.sb.exec(SETUP, timeout_seconds=300, check=True)

    def run(self, code: str) -> str:
        return self.sb.exec(["/home/user/v/bin/python", "-c", code], check=True).stdout

    def close(self):
        self.sb.delete()


def run_from_template(code: str) -> str:
    """(c) A new sandbox per call, started from a pre-warmed named snapshot."""
    sb = client.create(type="small", ttl_seconds=600, no_env=True, from_=TEMPLATE,
                       name="tool-call-c")
    try:
        return sb.exec(["/home/user/v/bin/python", "-c", code], check=True).stdout
    finally:
        sb.delete()


if __name__ == "__main__":
    code = "import sympy; print(sympy.isprime(2**61 - 1))"
    print("a:", run_fresh(code).strip())
    s = Session()
    try:
        print("b:", s.run(code).strip(), s.run("print(1 + 1)").strip())
        s.sb.snapshot(TEMPLATE)  # the warmed disk becomes the template for (c)
    finally:
        s.close()
    try:
        print("c:", run_from_template(code).strip())
    finally:
        print(client.api.delete_named_snapshot(TEMPLATE).type)
```

```text
a: True
b: True 2
c: True
snapshot.named.deleted
```

The whole file ran in 33 seconds (10:31:44 to 10:32:17 UTC), including the session's one-time install and the template save.

Notes on the code:

- `try`/`finally` around every fresh sandbox, so an exception in the tool still deletes it. `ttl_seconds=600` is the second net: if your process dies before `finally` runs, the sandbox stops itself after 10 minutes.
- `no_env=True` on the per-call sandboxes, because those are the ones that run untrusted input. The session sandbox here keeps your environment.
- Passing the command as a list quotes it for you, so the model's code can contain quotes.
- In production, build the template once in a deploy step, not in the agent. A template is a full disk copy, so it carries anything you wrote to the disk; keep secrets out of it. Rebuilding it when your dependencies change is covered in [Cache dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot).

## FAQ

### How long does a fresh sandbox add to a tool call?

In our 5 runs, a fresh `small` sandbox from a template took 3.0 to 3.3 seconds per call end to end, against 0.5 to 0.6 seconds for a call in an already-running sandbox. About 2.55 seconds of the difference is create to ready.

### Is a fork faster than starting from a named snapshot?

We didn't time forks on this page. In [our start, resume and fork benchmark](/guides/how-fast-does-a-cloud-sandbox-start-resume-and-fork), a fork took a median 3.35 seconds to ready, about the same as a new sandbox. A fork copies the disk only and boots fresh, like `--from`, and it needs a source sandbox to exist. A named snapshot has no running source, so it suits a per-call template better.

### Do I have to wait for delete to finish?

No. The delete call returned in about 30 ms in every run, and the sandbox is torn down in the background. Billing stops at delete.

### Does a reused sandbox leak data between calls?

Yes, by design: files, environment changes and running processes stay for the next call. That's the point of (b) when calls build on each other, and the reason to avoid it when calls come from different users.

### What if the agent crashes before deleting the sandbox?

The sandbox stops at its TTL, 1 hour by default, and stops billing compute. A stopped sandbox stays in your list until you delete it, so pass a short `ttl_seconds` and sweep leftovers by name.

## Related

- [Persistent or ephemeral sandboxes for agents](/guides/persistent-or-ephemeral-sandboxes-for-agents)
- [Turn a sandbox into a reusable template](/guides/turn-a-sandbox-into-a-reusable-template)
- [Best ways to run untrusted code safely](/guides/best-ways-to-run-untrusted-code-safely)
- [Python SDK](/docs/sdk-python)
