RFC: Allow fork from paused sandbox and named snapshots for RL/agent training workloads
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 1.6k
- Forks
- 438
- PR merge metrics
- No merged PRs in 30d
Description
Summary
e2b already ships all the primitives needed for RL agent training — pause, resume, fork, and named snapshots. One missing link prevents composing them into an efficient training loop: POST /sandboxes/{id}/fork returns 409 when the sandbox is paused, forcing callers to keep the parent sandbox running (burning a concurrency slot) or pay the full re-initialization cost every episode batch.
This RFC proposes two changes, in priority order.
Problem
The forced choice for RL training today
A typical RL training loop wants to:
- Spin up a sandbox, install dependencies, load model weights, set up dataset — once (takes 30–120 s)
- Pause it to preserve that initialized state as a reusable checkpoint
- Fork N independent episode sandboxes from that checkpoint, run rollouts
- Kill the episode sandboxes after each batch
- Repeat step 3 for the next batch — reusing the same checkpoint, zero re-initialization
This fails today at step 3:
POST /sandboxes/{id}/fork (sandbox is paused)
→ 409 "Sandbox 'X' is paused and cannot be forked; resume it first"
The workarounds both have real costs:
| Workaround | Cost |
|---|---|
| Keep parent sandbox running | Wastes one concurrency slot + billed for the entire training run |
| Resume → fork → pause per batch | ~2–4 s latency per batch; parent must never time out |
In RL training the environment reset latency directly stalls GPU utilization. Frameworks like AgentENV are built specifically around "snapshot once, derive many" — e2b has the identical block-layer infrastructure (COW memfd + overlay) but the API surface blocks the pattern.
Proposal A — Fork from paused sandbox (small change, high value)
Change: when POST /sandboxes/{id}/fork is called on a paused sandbox, skip CheckpointSandbox (the in-place pause→snapshot→resume step) and boot the fork children directly from the snapshot that already exists in the snapshot cache.
Affected file: packages/api/internal/handlers/sandbox_fork.go
The snapshot is already there — snapshotCache.Get(ctx, sandboxID) returns it. buildResumeSandboxDataFromSnapshot already accepts an arbitrary snapshot source ID. The only change is branching on the paused case instead of returning 409.
Behavior change:
| Source state | Before | After |
|---|---|---|
running |
✅ 201 — checkpoint in-place, fork N | ✅ 201 (unchanged) |
paused |
❌ 409 | ✅ 201 — use existing snapshot, fork N |
| killed / not found | ❌ 404 | ❌ 404 (unchanged) |
The parent stays paused. Each child boots independently from the COW snapshot, identical to today's running-sandbox fork path.
Proposal B — Fork directly from a named snapshot (medium effort)
POST /sandboxes/{id}/snapshots already creates persistent, named snapshots (my-team/init-state:v1) that outlive the original sandbox. There is currently no way to fork from one:
# works today
POST /sandboxes/{id}/snapshots → { snapshotID: "my-team/init-state:v1" }
GET /snapshots → list all snapshots
# proposed new endpoint
POST /snapshots/{snapshotID}/fork
Body: { count: 32, timeout: 300 }
→ 201 [ { sandboxID, host, ... }, ... × 32 ]
This decouples the fork source from any sandbox's lifetime entirely. A snapshot created once can seed thousands of episode batches over days or weeks, with no sandbox kept alive between them.
Why this matters
The RL and agent-training use case has a clear infrastructure shape: initialize once, branch cheaply, reset fast. e2b's COW block layer is already the right primitive. This proposal surfaces it through the API without adding new infrastructure.
Proposal A in particular is a small change (~50 lines in one file) that removes a 409 and unlocks the full workflow. Proposal B follows naturally once the fork-from-snapshot path is established.
Open questions for maintainers
- Is there a deliberate reason the paused-fork path was blocked — e.g. a snapshot consistency concern I'm not seeing?
- Should
maxForkCount = 100(currently a hard-coded constant insandbox_fork.go) become a per-team limit to support large-scale rollouts (256–512 parallel episodes)? - What is the intended TTL for named snapshots from
POST /sandboxes/{id}/snapshots? RL training runs last days to weeks and the snapshot needs to survive the whole run.
Happy to submit a PR for Proposal A if the direction looks right.
/cc @jakubno @dobrac @ValentaTomas @arkamar @tvi
Looking forward to your feedback.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with packages/api/internal/handlers/sandbox_fork.go, including the current paused-sandbox 409 path, snapshotCache.Get, buildResumeSandboxDataFromSnapshot, and the maxForkCount constant. Confirm with maintainers whether Proposal A or the named-snapshot endpoint is in scope before changing anything. Done means the agreed fork behavior and limits are specified and validated without regressing running, killed, or not-found cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100