Concepts

Forking sandboxes

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

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

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.

Branching experiments

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

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

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

sb.checkpoint()sb.fork()
Snapshots the containerYesYes
Writes sandbox.checkpoint_created to ledgerYesYes
Original sandbox keeps runningYesYes
Returns a new live sandboxNoYes
Use with client.resume()YesYes (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(), 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().