Snapshots and fork#

A snapshot captures a sandbox's RAM and disk at the same instant. Fork uses the same mechanism to clone a sandbox, including its memory, into a new, independent sandbox (target p95 < 2 s).

Three kinds of saved state#

Pause snapshot Named snapshot Template
Created by pause / auto-pause POST /sandboxes/{sandbox_id}/snapshots POST /templates
Lifetime until the sandbox is deleted until you delete it (outlives the sandbox) until you delete it
Contains RAM + disk RAM + disk (or disk only with memory: false) RAM + disk
Use resume the same sandbox POST /sandboxes with snapshot_id; base for a template POST /sandboxes with template
ID internal snap_... tpl_... / name

Storage for snapshots is free for 30 days (free_storage_until), then $0.03/GB-month on the stored size after compression and deduplication (storage_bytes).

Create a named snapshot#

A running sandbox is paused briefly (typically a few hundred milliseconds) and continues afterwards. A paused sandbox is snapshotted from its pause state.

const snap = await sandbox.snapshots.create({ name: "after-npm-install", labels: { repo: "web" } });
console.log(snap.id, snap.state); // snap_3kd7wq2mzn5x ready
snap = sandbox.snapshots.create(name="after-npm-install", labels={"repo": "web"})
curl -sS -X POST https://api.dormo.dev/v1/sandboxes/sbx_2fqxnt4bkq7m/snapshots \
  -H "Authorization: Bearer $DORMO_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"after-npm-install"}'

dormo snapshot create sbx_2fqxnt4bkq7m --name after-npm-install

Set memory: false for a smaller, disk-only snapshot. Sandboxes started from it boot instead of resuming memory.

Start a new sandbox from the snapshot:

const copy = await client.sandboxes.create({ snapshot_id: snap.id });
dormo create --snapshot snap_3kd7wq2mzn5x

List and delete: GET /snapshots (filter sandbox_id), GET /snapshots/{snapshot_id}, DELETE /snapshots/{snapshot_id}. Deleting a snapshot used by a template returns 409 snapshot_in_use.

Named snapshots are copied to replicated object storage (encrypted with your account's key) right after they are created. A sandbox created from a snapshot can start on any host, also when the host that took the snapshot is down. The first sandbox from a snapshot on a new host waits for the download; later ones start from the local copy.

A sandbox created from a snapshot is a copy of the original machine: limits that apply to the original sandbox (for example a network restriction) apply to the copy too, as with fork.

Fork#

POST /sandboxes/{sandbox_id}/fork creates a new sandbox from the current state of the source, memory included.

  • A running source is paused for a few hundred milliseconds while RAM and disk are captured, then continues.
  • A paused source is forked from its pause snapshot and stays paused.
  • The fork has its own IP, network namespace and copy-on-write disk. Changes in the fork never affect the source.
  • The fork has the same size as the source. You can set name, labels, env, auto_pause, priority, ttl_seconds, metadata.
  • forked_from on the new sandbox records sandbox_id and snapshot_id.
const fork = await sandbox.fork({ name: "attempt-1", labels: { run: "eval-17" } });
const r = await fork.exec({ command: "pytest -x" });
fork = sandbox.fork(name="attempt-1", labels={"run": "eval-17"})
r = fork.exec(command="pytest -x")
curl -sS -X POST https://api.dormo.dev/v1/sandboxes/sbx_2fqxnt4bkq7m/fork \
  -H "Authorization: Bearer $DORMO_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: eval-17-attempt-1" \
  -d '{"name":"attempt-1","labels":{"run":"eval-17"}}'

dormo fork sbx_2fqxnt4bkq7m --name attempt-1

Parallel attempts from one state#

Prepare a sandbox once (clone the repo, install dependencies, start services), then fork it for each attempt:

const base = await client.sandboxes.create({ template: "base" });
await base.exec({ command: "git clone https://github.com/acme/app && cd app && npm ci", timeout_seconds: 600 });
await base.pause();

const attempts = await Promise.all(
  [1, 2, 3, 4].map((i) => base.fork({ name: `attempt-${i}`, ttl_seconds: 3600 })),
);
// Or in one call, with a single capture of the source (see Bulk fork below):
// const attempts = await base.fork({ count: 4, ttl_seconds: 3600 });

Fork counts against your concurrency limit and the starts rate limit like any create.

Bulk fork#

For RL rollouts and evals, count creates up to 50 forks in one call from a single capture of the source (higher limits on request). The source pauses once, not once per fork.

  • All or nothing. If your concurrency limit or the capacity next to the source cannot take every fork, nothing is created: the call fails with 409 concurrency_limit_reached or 503 capacity_unavailable (retryable; consider priority: "batch").
  • Response. With count omitted or 1 you get one Sandbox, as before. With count > 1 you get {"data": [Sandbox, ...]} with exactly count items: 201 when every fork is running, 202 with wait: false or priority: "batch".
  • Batch priority. With priority: "batch" the forks are created in queued from one snapshot, billed at the batch rate and started as capacity frees up.
  • name, labels, env, metadata, auto_pause, priority and ttl_seconds apply to every fork. Each fork counts toward the concurrency limit; the call counts as one request in the starts rate limit.
const forks = await base.fork({ count: 20, labels: { run: "eval-17" }, ttl_seconds: 3600 }); // Sandbox[]
const results = await Promise.all(forks.map((f) => f.exec({ command: "pytest -x" })));
forks = base.fork(count=20, labels={"run": "eval-17"}, ttl_seconds=3600)  # list[Sandbox]
results = [f.exec(command="pytest -x") for f in forks]
curl -sS -X POST https://api.dormo.dev/v1/sandboxes/sbx_2fqxnt4bkq7m/fork \
  -H "Authorization: Bearer $DORMO_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: eval-17-rollouts" \
  -d '{"count":20,"labels":{"run":"eval-17"},"ttl_seconds":3600}'

dormo fork sbx_2fqxnt4bkq7m --count 20

What a snapshot does not cover#

  • Secrets you passed in env are part of the VM's memory and environment. A fork or a sandbox created from a snapshot inherits them; override with env on fork, or avoid baking secrets into templates.
  • The external IP address may change after resume or fork.
  • Active TCP connections to the outside world may be dropped by the remote side during a long pause.