Are Sandboxer Sandboxes a Fit for Eval and Research Teams?
Updated October 27, 2026 .md
Short answer: Build each task environment once and save it as a named snapshot. Start one fresh microVM per task with sbx new --from, cap parallel runs at your plan's concurrency and start limits, and copy each task's diff, test report and environment fingerprint out before you delete it. Billing is per second, CPU only.
The jobs, and what to use for each#
An eval or research team asks the same six things of a sandbox. Each row gives the short answer, the feature that does it, and the guide with a full run on prod.
| Job | Short answer | Sandboxer feature | Worked guide |
|---|---|---|---|
| A reproducible task image | Install once, save it under a versioned name, start every run from it | sbx snapshot <id> task-v1, sbx new --from task-v1 |
Cache dependencies in a named snapshot |
| A fresh environment per task | One VM per task, deleted after; nothing one task writes reaches the next | Named snapshot plus sbx delete <id> --yes; sbx fork to branch a running setup |
RL and eval environments, SWE-bench-style evals |
| Fan-out under caps | A client-side semaphore sized to your free places, paced under the per-minute start limit | sbx limits --json, 429 with Retry-After |
Rate limits and concurrency caps, Shard a test suite |
| Capturing artifacts and diffs | Write diff, JUnit and a fingerprint to one folder, copy it out, then delete | git diff, sbx scp <id>:<path> . |
Best-of-N with parallel forks |
| Cost per task | Seconds a VM is up times the size's rate; a short task costs fractions of a cent | Per-second metering, --ttl as a backstop |
Sandbox cost tables, Cap what agent sandboxes can cost |
| Knowing what isn't deterministic | Record CPU model and clock behavior with each result; don't stop a VM mid-task | lscpu, memoryRestored, resumeReason |
CPU performance by size, Detect a cold boot |
Snapshot or fork?#
sbx new --from <name> |
sbx fork <id> |
|
|---|---|---|
| Use it for | The fixed environment a whole benchmark shares | Branching from a state an agent just reached, such as best-of-N attempts |
| Needs the source sandbox | No | Yes |
| Versioned by | Its name (task-v1, task-v2); reusing a name replaces it |
The source's id at that moment |
| Memory and processes | Not copied: boots fresh | Not copied: boots fresh |
For a benchmark result you want to cite later, use a named snapshot with a version in the name. A fork points at a sandbox that will be gone.
One task per VM, end to end#
This is what we ran on prod on 2026-10-27 with small sandboxes. The task repo has two bugs, median and clamp, with two tests each. A task applies a candidate patch, runs its tests and writes four files. We used hand-written patches as the candidates. In a real run, the patch is your agent's diff.
The per-task script, baked into the image:
#!/bin/bash
# run_task.sh <task> : apply the candidate patch, run that task's tests, write ~/out/<task>/
set -u
task=$1; out=~/out/$task; mkdir -p $out
cd ~/env/repo
git apply ../patches/$task.diff && applied=true || applied=false
git diff > $out/patch.diff
t=$(date +%s.%N)
../.venv/bin/python -m pytest -q -p no:cacheprovider --junitxml=$out/junit.xml test_$task.py > $out/pytest.txt 2>&1; rc=$?
secs=$(echo "$(date +%s.%N) - $t" | bc)
cat > $out/env.json <<J
{"cpu": "$(lscpu | sed -n 's/^Model name: *//p')", "kernel": "$(uname -r)", "python": "$(../.venv/bin/python -V | cut -d' ' -f2)", "deps": "$(../.venv/bin/pip freeze | sha256sum | cut -c1-12)", "repo": "$(git rev-parse HEAD | cut -c1-12)"}
J
echo "{\"task\": \"$task\", \"applied\": $applied, \"pytestExit\": $rc, \"testSeconds\": $secs, \"summary\": \"$(tail -1 $out/pytest.txt)\"}" > $out/result.json
tar czf ~/out/$task.tgz -C ~/out $task
cat $out/result.json1. Build the image once#
sbx --json new --name eval-1 --type small --ttl 3600 | jq -r 'select(.event=="ready") | .id'
id=sb_exupvsjls5
sbx exec $id "echo $(base64 -w0 env.tgz) | base64 -d | tar xz && chmod +x env/run_task.sh"
sbx exec $id --timeout 300 "cd env && python3 -m venv .venv && .venv/bin/pip install -q pytest==8.3.3 && .venv/bin/python -m pytest --version \
&& cd repo && git init -q && git add -A && git -c user.email=eval@example.com -c user.name=eval commit -qm task-v1 \
&& git rev-parse --short=12 HEAD"
sbx exec $id "cd env/repo && ../.venv/bin/python -m pytest -q -p no:cacheprovider --tb=no | tail -1"pytest 8.3.3
08d894442ecd
3 failed, 1 passed in 0.01sThe baseline fails 3 of 4 tests, so the grader works before any patch touches it. Commit the task repo inside the image: git diff then shows exactly what the candidate changed, and the commit hash goes into every result.
2. Freeze it and delete the builder#
sbx --json snapshot $id eval-task-v1
sbx delete $id --yes{"ok":true,"type":"snapshot.named.info","snapshot":{"name":"eval-task-v1","snapshotId":"7396f9c8-…","sourceSandboxId":"sb_exupvsjls5","status":"ready","type":"small",…}}3. Run two tasks in parallel and collect results#
#!/bin/bash
# fanout.sh: one sandbox per task from the named snapshot, collect artifacts, delete.
snap=eval-task-v1
run_one() { # $1 = task, $2 = sandbox name
t0=$(date +%s.%N)
id=$(sbx --json new --from $snap --name $2 --type small --ttl 900 | jq -r 'select(.event=="ready") | .id')
t1=$(date +%s.%N)
sbx exec $id "~/env/run_task.sh $1" > /dev/null
sbx scp $id:/home/user/out/$1.tgz results/ > /dev/null
t2=$(date +%s.%N)
sbx delete $id --yes > /dev/null
t3=$(date +%s.%N)
printf '%s %s start=%.2fs run+collect=%.2fs delete=%.2fs total=%.2fs\n' $1 $id \
$(echo "$t1-$t0" | bc) $(echo "$t2-$t1" | bc) $(echo "$t3-$t2" | bc) $(echo "$t3-$t0" | bc)
}
mkdir -p results
s=$(date +%s.%N)
run_one median eval-2 &
run_one clamp eval-3 &
wait
printf 'wall=%.2fs\n' $(echo "$(date +%s.%N)-$s" | bc)/home/user/env/run_task.sh: line 10: bc: command not found
/home/user/env/run_task.sh: line 10: bc: command not found
median sb_x7plngnio6 start=3.19s run+collect=3.64s delete=2.13s total=8.96s
clamp sb_bryg2ymo9f start=3.19s run+collect=3.76s delete=2.13s total=9.09s
wall=9.10sThe bc errors are a real lesson. bc is on our machine but not in the sandbox image, so testSeconds came out empty. That is the reproducibility problem in miniature: the image has only what you put in it. Install every tool a task calls before you snapshot, or use one the image has (python3). pytest's own time is in junit.xml either way.
What came back in results/median.tgz:
$ cat median/result.json
{"task": "median", "applied": true, "pytestExit": 0, "testSeconds": , "summary": "2 passed in 0.01s"}
$ cat median/env.json
{"cpu": "AMD Ryzen 9 7950X3D 16-Core Processor", "kernel": "6.18.56-sandboxer", "python": "3.12.3", "deps": "90531af91104", "repo": "08d894442ecd"}
$ cat median/patch.diff
diff --git a/calc.py b/calc.py
index e7a86df..a9f4cea 100644
--- a/calc.py
+++ b/calc.py
@@ -1,6 +1,9 @@
def median(xs):
- xs = list(xs)
- return xs[len(xs) // 2]
+ xs = sorted(xs)
+ n = len(xs)
+ if n % 2:
+ return xs[n // 2]
+ return (xs[n // 2 - 1] + xs[n // 2]) / 2Both tasks reported the same env.json: the same CPU model, kernel, Python, dependency hash and repo commit. That fingerprint is what lets you compare two results, or rerun one later and know you ran the same thing. Afterward, sbx --json info on each id returned 404 not_found, and sbx snapshot rm eval-task-v1 removed the image.
Timings and cost#
Single measurements, timed from our client, so they include CLI and network time:
| Step | Time | Machine time billed |
|---|---|---|
Create the builder (small) |
3.19 s to ready | |
| venv, pytest 8.3.3, git commit | 4.20 s | |
| Save the named snapshot | 2.13 s | |
| Builder's whole life, create to delete | About 22 s | About $0.00011 |
| Each task: start, run, copy out, delete | 8.96 s and 9.09 s | About $0.000045 each |
| Both tasks in parallel | 9.10 s of wall time |
small costs $0.018/hour, metered per second while the sandbox is up. At about 9 seconds a task, 1,000 such tasks cost about $0.05 of machine time. A real task is longer: 10 minutes on default ($0.036/hour) costs $0.006, and on large ($0.072/hour) $0.012. The setup cost is paid once per image version, not per task.
Fan-out math before a big run#
Creates, resumes and forks each count as one start. The plan limits, from billing:
| Plan | Running at once | Starts per minute / hour / day | 1,000 one-VM tasks |
|---|---|---|---|
| Free trial | 2 (small and default only, TTL up to 8 hours) |
Batch tasks per VM | |
| $20 | 100 | 12 / 60 / 200 | 5 days of starts: batch several tasks per VM |
| $100 | 300 | 30 / 210 / 840 | Over the daily cap: about 4 hours for 840, the rest the next day, or batch |
Two ways to stay inside the limits:
- One task per VM when tasks are untrusted agent output, or when one task's leftovers could change another's score. This is the default for agent benchmarks.
- Several tasks per VM, run one after another and reset between them (
git checkout -- . && git clean -fdx) when you trust the tasks and are short on starts. You lose kernel-level isolation between those tasks, so don't do it for agent patches that run arbitrary code.
Check sbx limits --json | jq '.starts' before a burst. A 429 rate_limited carries Retry-After and details.resetsAt; a 429 limit_reached means too many are running, so queue instead of retrying.
Limits to plan around#
- CPU model can vary by machine. Every sandbox we started on 2026-10-27 reported an AMD Ryzen 9 7950X3D, but that is not a promise. Record
lscpuwith each result, and don't compare wall-clock speed across runs without checking it. Single-thread speed was about the same on every size in our CPU measurements. - Clocks. The wall clock is corrected on resume. Elapsed-time clocks may not count all of the stopped time: in a test on 2026-10-27, a 2-minute stop moved the guest's monotonic clock forward by only about 62 seconds. In another test the same day, a sandbox's clock was 1.18 s behind ours. Don't stop a VM in the middle of a timed task, and don't compute latencies from sandbox timestamps against your own.
- Resume can lose memory. A resume on a machine with a different CPU type boots from disk: files kept, processes lost,
memoryRestored: falsewithresumeReason: "cpu_mismatch".sbx resume --require-memorywaits instead. For evals, avoid the question: run each task start to finish in one fresh VM. - The network is live. A task that installs packages or pulls images by tag at run time can get different bytes tomorrow. Install and pull before the snapshot, pin versions, and put the dependency hash in the result.
- No GPU. Sandboxes are CPU-only:
small2 vCPU/4 GB,default4/8,large8/16,xlarge16/32 (needs the $100 plan). Run model inference elsewhere and use the sandbox for the code the model writes. - Moving to a smaller size isn't supported yet. Create a new, smaller sandbox and copy what you need into it.
FAQ#
How do I make sure every task sees the same environment?#
Start every task from one named snapshot, and record a fingerprint with each result: the repo commit, a hash of pip freeze (or your lockfile), the kernel and the CPU model. In our run both tasks reported identical fingerprints. Bump the snapshot name when anything in it changes.
Can a candidate patch see another task's files or hidden tests?#
Not with one task per VM. Each task runs in its own microVM booted from the snapshot, and it is deleted after. Keep hidden tests out of the image and copy them in only after the agent finishes, as the RL and eval guide shows.
How do I get results out before deleting the sandbox?#
Write everything a task produces into one folder, tar it, and copy it out with sbx scp <id>:/home/user/out/<task>.tgz .. Delete the sandbox only after the copy succeeds, and give each one a short --ttl so a crashed runner can't leave it running.
Can I run SWE-bench itself?#
Yes. Each sandbox has its own Docker daemon, so the official harness runs unchanged. The SWE-bench guide grades a real SWE-bench Lite task on large and fans out from a snapshot with the task image already pulled.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.