# 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.

```ts
const snap = await sandbox.snapshots.create({ name: "after-npm-install", labels: { repo: "web" } });
console.log(snap.id, snap.state); // snap_3kd7wq2mzn5x ready
```

```python
snap = sandbox.snapshots.create(name="after-npm-install", labels={"repo": "web"})
```

```bash
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:

```ts
const copy = await client.sandboxes.create({ snapshot_id: snap.id });
```

```bash
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_from` on the new sandbox records `sandbox_id` and `snapshot_id`.

```ts
const fork = await sandbox.fork({ name: "attempt-1", labels: { run: "eval-17" } });
const r = await fork.exec({ command: "pytest -x" });
```

```python
fork = sandbox.fork(name="attempt-1", labels={"run": "eval-17"})
r = fork.exec(command="pytest -x")
```

```bash
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:

```ts
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.

```ts
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" })));
```

```python
forks = base.fork(count=20, labels={"run": "eval-17"}, ttl_seconds=3600)  # list[Sandbox]
results = [f.exec(command="pytest -x") for f in forks]
```

```bash
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 `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.

## Related

- [Lifecycle](lifecycle.md)
- [Templates](templates.md)
- [Billing](../billing.md)
