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.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/snapshots \
-H "Authorization: Bearer $TASKCLOUD_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"after-npm-install"}'
taskcloud 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 });
taskcloud 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_fromon the new sandbox recordssandbox_idandsnapshot_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.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/fork \
-H "Authorization: Bearer $TASKCLOUD_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: eval-17-attempt-1" \
-d '{"name":"attempt-1","labels":{"run":"eval-17"}}'
taskcloud 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_reachedor503 capacity_unavailable(retryable; considerpriority: "batch"). - Response. With
countomitted or1you get oneSandbox, as before. Withcount > 1you get{"data": [Sandbox, ...]}with exactlycountitems:201when every fork is running,202withwait: falseorpriority: "batch". - Batch priority. With
priority: "batch"the forks are created inqueuedfrom one snapshot, billed at the batch rate and started as capacity frees up. name,labels,env,metadata,auto_pause,priorityandttl_secondsapply to every fork. Each fork counts toward the concurrency limit; the call counts as one request in thestartsrate 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.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/fork \
-H "Authorization: Bearer $TASKCLOUD_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: eval-17-rollouts" \
-d '{"count":20,"labels":{"run":"eval-17"},"ttl_seconds":3600}'
taskcloud fork sbx_2fqxnt4bkq7m --count 20
What a snapshot does not cover#
- Secrets you passed in
envare part of the VM's memory and environment. A fork or a sandbox created from a snapshot inherits them; override withenvon 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.