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:
- Calls
createSnapshoton the current container and waits for the snapshot to be ready - Writes a
sandbox.checkpoint_createdevent to the ledger (same assb.checkpoint()) - 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 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(), 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().