e2b-dev / e2b-dev/runtime

RFC: Allow fork from paused sandbox and named snapshots for RL/agent training workloads

Open
#3,424 0 comments 0 reactions 0 assignees View on GitHub

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:

  1. Spin up a sandbox, install dependencies, load model weights, set up dataset — once (takes 30–120 s)
  2. Pause it to preserve that initialized state as a reusable checkpoint
  3. Fork N independent episode sandboxes from that checkpoint, run rollouts
  4. Kill the episode sandboxes after each batch
  5. 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

  1. Is there a deliberate reason the paused-fork path was blocked — e.g. a snapshot consistency concern I'm not seeing?
  2. Should maxForkCount = 100 (currently a hard-coded constant in sandbox_fork.go) become a per-team limit to support large-scale rollouts (256–512 parallel episodes)?
  3. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.