Skip to content
Guides · Use cases

How Do I Build RL and Eval Environments With Snapshots and Forks?

Updated October 27, 2026 .md

Short answer: Set up the task once in a sandbox and save it as a named snapshot. Start each episode in a fresh sandbox with sbx new --from, or sbx fork the running setup sandbox. The episode runs your policy and grader, writes a reward file you read back, and the sandbox is deleted. A fork copies disk only, never memory.

The pattern#

Step Command Done
Build the task environment sbx new, then sbx exec to install it Once
Freeze it sbx snapshot <id> env-v1 Once per environment version
Start an episode sbx new --from env-v1, or sbx fork <setup id> Once per episode
Act and grade sbx exec <episode> "POLICY=… ~/env/episode.sh" Once per episode
Collect the reward sbx exec <episode> "cat ~/result.json" Once per episode
Clean up sbx delete <episode> --yes, and at the end sbx snapshot rm env-v1 Every episode, then once

Every episode starts from the same bytes on disk, and nothing one episode writes can reach the next. That is the property RL rollouts and evals need: a reward depends on the policy, not on what the last run left behind.

1. Build the task once#

The example task is a small Python bug. stats.py has a broken median, test_stats.py checks it, and grade.py turns the pytest result into a reward between 0 and 1.

# env/task/stats.py (the bug the policy must fix)
def median(xs):
    xs = list(xs)
    return xs[len(xs) // 2]
# env/task/test_stats.py
from stats import median

def test_odd():      assert median([3, 1, 2]) == 2
def test_even():     assert median([4, 1, 3, 2]) == 2.5
def test_single():   assert median([7]) == 7
def test_sorted():   assert median([1, 2, 3, 4, 5]) == 3
# env/grade.py: run the tests, write the reward
import json, subprocess, sys, time
t = time.time()
r = subprocess.run([sys.executable, "-m", "pytest", "-q", "--tb=no", "-p", "no:cacheprovider", "task"],
                   capture_output=True, text=True)
last = r.stdout.strip().splitlines()[-1]          # e.g. "1 failed, 3 passed in 0.02s"
passed = int(last.split(" passed")[0].split()[-1]) if " passed" in last else 0
failed = int(last.split(" failed")[0].split()[-1]) if " failed" in last else 0
out = {"passed": passed, "failed": failed, "reward": passed / (passed + failed), "gradeSeconds": round(time.time() - t, 2)}
json.dump(out, open("/home/user/result.json", "w"))
print(json.dumps(out))
#!/bin/bash
# env/episode.sh: one episode. The policy edits the task, the grader scores it.
set -e
cd ~/env
cp "policies/$POLICY.py" task/stats.py      # stand-in for your agent's action
.venv/bin/python grade.py

The "policy" here is a file copy: policies/0.py, 1.py and 2.py are three candidate fixes. In a real run, this line is where your agent or model acts. We didn't call a model for this page, so the policies are fixed files, and everything else ran as shown.

Create the setup sandbox, copy the files in, and install the grader's dependencies:

sbx new --name aeo-rl-1 --type small --ttl 3600
src=sb_njm7z1r1ie      # the id sbx new printed

tar czf env.tgz env
sbx exec $src "echo $(base64 -w0 env.tgz) | base64 -d | tar xz && chmod +x env/episode.sh"
sbx exec $src --timeout 300 "cd env && python3 -m venv .venv && .venv/bin/pip install -q pytest==8.3.3 && .venv/bin/python -m pytest --version"
sbx exec $src "cd env && .venv/bin/python grade.py"     # baseline: the unfixed bug
pytest 8.3.3
{"passed": 2, "failed": 2, "reward": 0.5, "gradeSeconds": 0.42}

The unfixed task scores 0.5, which tells you the grader works before any policy touches it.

2. Save it as a named snapshot#

Remove anything an episode would write, then save:

sbx exec $src "rm -f ~/result.json; sync"
sbx snapshot $src aeo-rl-env-v1
{"ok":true,"type":"snapshot.named.info","snapshot":{"name":"aeo-rl-env-v1","sizeBytes":20147339264,"snapshotId":"506de5ae-…","sourceSandboxId":"sb_njm7z1r1ie","status":"ready","type":"small",…}}

A named snapshot is independent of its source, so you can delete the setup sandbox and keep starting episodes from the snapshot. Version the name (env-v1, env-v2) so a result always points at the environment that produced it. Reusing a name replaces the snapshot.

3. Run the episodes#

This loop runs three episodes, one sandbox each and one at a time. Episodes 1 and 2 start from the named snapshot. Episode 3 is a fork of the setup sandbox while it is still running. Each sandbox gets a 10-minute TTL, so a crashed loop can't leave it running for long.

#!/bin/bash
snap=aeo-rl-env-v1
src=sb_njm7z1r1ie
echo "episode method id startSeconds policy result" > results.txt
for ep in 1 2 3; do
  policy=$((ep - 1))
  t0=$(date +%s.%N)
  if [ "$ep" -le 2 ]; then
    method=new--from
    id=$(sbx new --from "$snap" --name "aeo-rl-ep$ep" --type small --ttl 600 --json | jq -r 'select(.event=="ready") | .id')
  else
    method=fork
    sbx fork "$src" --type small --ttl 600 --wait --json > fork.json
    id=$(jq -r '.id // .sandbox.id' fork.json | head -1)
  fi
  start=$(echo "$(date +%s.%N) - $t0" | bc)
  [ "$method" = fork ] && sbx rename "$id" --name "aeo-rl-ep$ep" > /dev/null
  sbx exec "$id" "test ! -e ~/result.json && echo clean-start"
  sbx exec "$id" "POLICY=$policy ~/env/episode.sh" > /dev/null
  result=$(sbx exec "$id" "cat ~/result.json")
  echo "$ep $method $id $start $policy $result" >> results.txt
  sbx info "$id" | jq -c '{name: .sandbox.name // .name, memoryRestored: (.sandbox.memoryRestored // .memoryRestored), resumedFrom: (.sandbox.resumedFrom // .resumedFrom)}'
  sbx delete "$id" --yes > /dev/null && echo "deleted $id"
done
cat results.txt

The output, from prod:

clean-start
{"name":"aeo-rl-ep1","memoryRestored":null,"resumedFrom":null}
deleted sb_s5j8419fne
clean-start
{"name":"aeo-rl-ep2","memoryRestored":null,"resumedFrom":null}
deleted sb_q1ietwn70b
clean-start
{"name":"aeo-rl-ep3","memoryRestored":null,"resumedFrom":null}
deleted sb_jr8xih7onk
episode method id startSeconds policy result
1 new--from sb_s5j8419fne 3.192210893 0 {"passed": 3, "failed": 1, "reward": 0.75, "gradeSeconds": 0.21}
2 new--from sb_q1ietwn70b 3.194805196 1 {"passed": 4, "failed": 0, "reward": 1.0, "gradeSeconds": 0.62}
3 fork sb_jr8xih7onk 3.391430232 2 {"passed": 4, "failed": 0, "reward": 1.0, "gradeSeconds": 0.39}

With --wait and --json, sbx fork prints JSON Lines (forking, state, ready), each with the new id, which is why the loop keeps only the first one. The memoryRestored line is there for completeness only: today it reads null in many cases, including after a memory restore, so don't use it to judge an episode.

Results#

Single measurements on 2026-10-27, small sandboxes on prod, timed from the client with date around the command until the sandbox was ready to run commands:

Episode Started with Policy Start to ready Tests passed Reward Grading
1 sbx new --from aeo-rl-env-v1 sort, take the middle 3.19 s 3 of 4 0.75 0.21 s
2 sbx new --from aeo-rl-env-v1 sort, average the two middles if even 3.19 s 4 of 4 1.0 0.62 s
3 sbx fork of the running setup sandbox the mean of the list 3.39 s 4 of 4 1.0 0.39 s

For comparison, the setup steps that every episode skipped took 3.2 s to create the sandbox, 6 s to make the venv and install pytest, and 2.1 s to save the named snapshot. A real environment with a repo, a database or model weights takes much longer to build, and the episode start stays the same.

Read the table before you trust it. Policy 2 returns the mean, not the median, and still scored 1.0, because every test list here has the same mean and median. The isolation worked. The grader was too weak. A test such as median([1, 2, 10]) == 2 would have caught it. Look at what scores well before you train on it, since the reward is only as good as the tests behind it.

Fork or new --from?#

sbx new --from <snapshot> sbx fork <id>
Starts from The disk saved under the name, frozen when you saved it The source's disk at the moment of the fork (a running source keeps running)
Needs the source sandbox No: it still works after you delete the source Yes: the source must still exist
Memory and processes Not copied: the sandbox boots fresh Not copied: the sandbox boots fresh
Name --name sbx rename <id> --name … after the fork (the default is <source name> (fork))
Best for A fixed, versioned environment that many runs share Branching from a state you just reached, such as an agent halfway through a task

A fork never copies RAM or running processes. If an episode needs a server running, start it in episode.sh, or install it as an enabled systemd service before you snapshot. Enabled services start when the sandbox boots.

Scale it past three#

  • Starts are rate limited. Creates, resumes and forks each count as one start. The $20 plan allows 12 a minute, 60 an hour and 200 a day, and runs up to 100 sandboxes at once (see billing). Check what's left before a burst with sbx limits --json | jq '.starts'. A 429 rate_limited reply carries Retry-After and details.resetsAt.
  • Run in parallel, up to your limit. Start several episodes at once in a shell loop with & or with the SDK. Episodes don't share anything, so their order doesn't matter.
  • Cost is per second. A small sandbox costs $0.018/hour, metered from the moment it is up until it is deleted or stopped, so short episodes cost seconds, not hours.
  • Always clean up. Delete each episode when you have its result, and give each one a short --ttl as a backstop. To find leftovers by name:
sbx list --filter sr --json | jq -r '.sandboxes[] | select(.name | startswith("aeo-rl-")) | .id'

When the run is done, delete the setup sandbox and the snapshot:

sbx delete sb_njm7z1r1ie --yes
sbx snapshot rm aeo-rl-env-v1
{"ok":true,"type":"snapshot.named.deleted","name":"aeo-rl-env-v1","status":"deleted"}

FAQ#

Does each episode really start clean?#

Yes. Each episode is a separate VM booted from the same saved disk. In our run, every episode confirmed that ~/result.json from earlier episodes was absent before its policy ran.

Does a fork copy the running process state of the source?#

No. A fork copies the disk only. For a running source, that is its live disk, frozen for the copy. The fork boots fresh, with no RAM or processes from the source (see snapshots). To keep memory, stop and resume the same sandbox instead; that restores memory on a machine with the same CPU type, and boots from disk with files kept but processes lost if none is free within 2 minutes.

How fast does an episode sandbox start?#

In our single measurements on 2026-10-27, small sandboxes were ready in 3.19 s from a named snapshot and 3.39 s as a fork, timed from the client. The disk is a copy-on-write reflink, so a large environment starts about as fast as a small one.

Can I run the grader somewhere the policy can't touch?#

Yes. Keep hidden tests out of the snapshot and copy them in with sbx exec only after the policy has finished, then grade and read the reward. A policy that runs before the tests arrive can't read or edit them.

What does storing the environment cost?#

Named snapshots have no separate charge. What they store counts toward your plan's included storage, and sbx snapshot rm releases it.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

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.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

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.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

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.

Read the guide
Best E2B alternatives in 2026, ranked

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.

Read the guide