# Forking sandboxes
URL: /docs/core/concepts/forking

Create independent sandbox copies from a live checkpoint with sb.fork().



`sb.fork()` snapshots the current sandbox and returns a new independent `SandboxHandle` from that state — without closing the original. Both containers keep running from the same filesystem point.

## Basic usage [#basic-usage]

```ts
const sb = await client.sandbox({
  image: "node:22",
  resources: { cpu: "1", memory: "512Mi" },
  name: "my-job",
});

try {
  await sb.exec("npm ci");

  const fork = await sb.fork("after-install");
  try {
    // Run different workloads in parallel on the same base state
    await Promise.all([
      sb.exec("npm test").pipe(process.stdout),
      fork.exec("npm run build").pipe(process.stdout),
    ]);
  } finally {
    await fork.close();
  }
} finally {
  await sb.close();
}
```

The optional string argument is a tag stored in the ledger (same as `sb.checkpoint(tag)`). Both the original and the fork remain live and independent after the call.

## How it works [#how-it-works]

`sb.fork(tag?)` does three things:

1. Calls `createSnapshot` on the current container and waits for the snapshot to be ready
2. Writes a `sandbox.checkpoint_created` event to the ledger (same as `sb.checkpoint()`)
3. Spins up a new container from that snapshot and returns it as a `SandboxHandle`

The forked sandbox gets a new `sandboxId` and a ledger session named `fork-<parentName>-<shortId>`. The original sandbox is unaffected.

If the original sandbox has any credentials registered via `sb.credentials.set()`, the fork carries them over automatically — see [Credentials](/docs/core/concepts/credentials).

## Branching experiments [#branching-experiments]

Fork is well-suited for running diverging experiments from a shared base without rebuilding:

```ts
const env = client.environment("python-base", {
  image: "python:3.11-slim",
  resources: { cpu: "1", memory: "1Gi" },
  setup: async (sb) => {
    await sb.exec("pip install -q numpy");
  },
});

const sb = await env.sandbox();
try {
  // Fork into two independent experiment tracks
  const [forkA, forkB] = await Promise.all([sb.fork("track-a"), sb.fork("track-b")]);

  await Promise.all([
    forkA
      .exec("pip install -q pandas")
      .then(() =>
        forkA.exec("python3 -c 'import pandas; print(pandas.__version__)'").pipe(process.stdout),
      )
      .finally(() => forkA.close()),

    forkB
      .exec("pip install -q torch --index-url https://download.pytorch.org/whl/cpu")
      .then(() =>
        forkB.exec("python3 -c 'import torch; print(torch.__version__)'").pipe(process.stdout),
      )
      .finally(() => forkB.close()),
  ]);
} finally {
  await sb.close();
}
```

## Concurrency [#concurrency]

Each forked sandbox acquires a concurrency slot (counts against `SandboxClientOptions.maxConcurrency`). The original sandbox holds its slot as well. If you fork N times from one sandbox, you need N+1 available slots.

## Relationship to checkpoint and resume [#relationship-to-checkpoint-and-resume]

|                                               | `sb.checkpoint()` | `sb.fork()`                 |
| --------------------------------------------- | ----------------- | --------------------------- |
| Snapshots the container                       | Yes               | Yes                         |
| Writes `sandbox.checkpoint_created` to ledger | Yes               | Yes                         |
| Original sandbox keeps running                | Yes               | Yes                         |
| Returns a new live sandbox                    | No                | Yes                         |
| Use with `client.resume()`                    | Yes               | Yes (snapshot is in ledger) |

Because `fork()` writes a `sandbox.checkpoint_created` event, the resulting snapshot is visible in `sb.listCheckpoints()` and can also be used with `client.resume()`.

`sb.fork()` itself is really `sb.checkpoint()` followed by [`client.restoreSnapshot()`](/docs/core/api-reference/sandbox#restoresnapshot), not `client.resume()` — the new sandbox starts with a clean exec history and does not replay any prior execs from the ledger, unlike `resume()`. The table above still applies since the same underlying snapshot is usable with either call afterwards; it's `fork()`'s own behavior that matches `restoreSnapshot()`, not `resume()`.
