Snapshots & forks
Per-minute incremental encrypted snapshots, lazy restore with prefetch, and disk-only fork semantics.
Sandal snapshots every running sandbox continuously, not as a manual "save" step you have to remember, but as a background guarantee. This page covers what's actually captured, how restore gets fast again, and what forking does and doesn't copy.
Per-minute incremental encrypted snapshots#
While a sandbox is running or paused, Sandal takes an incremental snapshot roughly once a minute. Each snapshot only writes the blocks that changed since the last one (V4/RAFS content-addressed layering), so the steady-state cost of snapshotting is proportional to your write rate, not your disk size.
- Incremental: only dirty blocks are written; a full restore replays the chain back to a base image.
- Encrypted: snapshots are encrypted at rest with per-sandbox keys before they leave the host.
- Automatic: no API call required, it happens on the minute for any sandbox that isn't
deleted. - Retained across
pause,stop, andarchive: the state you had a minute before any of those transitions is recoverable.
What's captured#
- Full root filesystem (everything under
/, including anything you wrote via the files API) - Installed packages, build artifacts,
node_modules, virtualenvs, whatever's on disk - Process working directory and environment variables set at snapshot time (as filesystem/process state, not live memory, except for
pause, see below)
What's excluded#
- In-flight network connections, a snapshot can't preserve an open TCP socket; anything mid-request needs to be retried after restore.
- Live memory state, except for an explicit
pause. Routine per-minute snapshots are disk-level, not memory-level, restoring from one reboots the kernel fresh. If you need exact in-memory continuity (a REPL's variables, an open file descriptor table), that's whatpause/resumegives you, not the background snapshot chain. - Ephemeral mounts you've explicitly marked scratch (tmpfs-backed paths), if configured, these are wiped by design since they're meant to not survive a restart anyway.
- Secrets injected only as environment variables at boot that aren't also persisted to disk, if you exported a token in a shell session but never wrote it anywhere, and the shell state itself isn't the thing being restored (see previous bullet), don't assume it round-trips.
Lazy restore + prefetch playbooks#
Restoring a full disk image before letting you touch a sandbox would make every restore as slow as the sandbox's total disk usage. Sandal doesn't do that, restore is lazy: the sandbox boots against the snapshot chain immediately, and blocks are pulled in on first access, on demand.
That's fast to start but can be slow on first touch for a cold sandbox that's never been warmed. Prefetch playbooks close that gap: a playbook is a recorded access pattern (which blocks a typical boot + first request actually touch) that Sandal replays speculatively, in parallel, right after a lazy restore starts, so by the time your exec or expose()'d request actually needs those blocks, they're often already local.
const sb = await sandal.sandboxes.resume(sandboxId, {
prefetch: "playbook:node-24-web-boot", // built-in, or record your own
});Playbooks are per-template and get more accurate the more a template is used. Sandal profiles real restores and refines the block-access trace over time. You don't need to hand-tune this for standard templates; it only becomes worth touching if you have an unusual first-request access pattern (e.g., a huge static asset directory scanned on boot) and want a custom playbook recorded against it.
Fork semantics#
A fork creates a new sandbox that starts from the exact disk state of the parent at the moment of forking, same files, same installed packages, same everything on disk, but is otherwise a clean sandbox, not a shared one.
const parent = await sandal.sandboxes.create({ template: "node-24" });
await parent.exec("pnpm install"); // do expensive setup once
const childA = await parent.fork();
const childB = await parent.fork();
// childA and childB both have node_modules already installed,
// and can now diverge independentlyFresh identity and credentials#
A fork is not a clone of the parent's identity. Every fork gets:
- A new sandbox ID, independent from the parent's
- A new set of DPoP-scoped tokens for its own preview URLs, the parent's
expose()tokens do not carry over and will not route to the child - Its own TTL clock and lifecycle state, decoupled from the parent
If the parent's disk happens to contain long-lived credentials (an API key baked into a config file, an SSH key on disk), those bytes are copied, fork inherits disk contents, not process identity. Rotate anything sensitive per-child if forks are meant to have distinct blast radii.
v1 scope: disk-only#
Today, fork is disk-only, it copies filesystem state via the snapshot chain, and the child boots fresh from that disk (same cold/lazy-restore path as any other sandbox creation). It does not copy:
- Live memory / process state of the parent (an in-progress computation in the parent's RAM does not appear in the child)
- Open network connections or PTY sessions
- The parent's
runningvspauseddistinction, the child always starts from a boot, not a memory resume
For workloads that need many cheap variations of a fully-set-up environment (parallel test matrices, per-branch preview sandboxes, agent tree-search over a shared starting state), disk-only fork is already the expensive part solved: you pay the setup cost once and fork it N times in roughly constant time per fork, instead of re-running setup N times.
Roadmap: live fork#
Live fork, forking a running sandbox's memory state, not just its disk, so a child can resume mid-computation with an identical process image, is on the roadmap. It's a strictly harder problem than disk fork (memory pages, open file descriptors, and in-flight syscalls all need to be consistent at the fork boundary) and isn't available yet. Disk-only fork remains the supported primitive until then; design your workflows around "fork, then re-run the fast part" rather than "fork mid-computation."
Failure policy: never discard work#
Sandal's snapshot and restore paths are built around one hard rule: a failure in the platform should never be the reason your sandbox's work disappears.
Concretely:
- If a snapshot write fails partway through, the previous complete snapshot in the chain remains the restore point. Sandal never leaves a sandbox pointing at a half-written snapshot.
- If a host fails while a sandbox is
running, the last successful per-minute snapshot is the recovery point; you lose at most the last snapshot interval's writes, not the sandbox. archive()anddelete()are treated very differently on purpose:archiveis designed to be reversible (cold storage, restorable),deleteis the only operation that's actually destructive, and it requires an explicit call, no automatic process ever deletes a sandbox's data as a side effect of being idle, over TTL, or out of quota. Auto-pause is what happens to neglected sandboxes, not auto-delete.
If you ever hit a case where platform behavior discarded sandbox state without an explicit delete() call from you, that's a bug, report it, don't work around it.