all posts

Snapshot-Restore vs Fork: When to Use Which

Ajay Kumar··8 min read

Snapshot-restore and fork sit on the exact same PandaStack primitive — a Firecracker snapshot restored with copy-on-write memory and disk — yet they answer two different questions. Snapshot-restore asks: give me a fresh, clean machine that looks exactly like the template did the moment it was baked. Fork asks: give me a copy of this specific machine, right now, with everything it has done so far still loaded. Both land fast, both share pages rather than copy bytes, and it is easy to conflate them because the mechanism is shared. But choosing the wrong one either wastes the warm state you paid for, or hands you N copies of a mess you wanted to escape. This post is the decision guide: what they share, how they differ semantically, their latency and placement profiles, and a clean set of rules for which to reach for.

One-line version: restore boots a FRESH VM from the generic template snapshot (deterministic, identical every time). Fork clones a SPECIFIC running VM's live state (its memory + disk at that instant). Restore for clean per-request sandboxes; fork for branching an already-warmed environment.

The mechanism they share

Before the difference, the sameness — because it explains why both are fast and why it is tempting to treat them as one thing. Both restore a Firecracker snapshot: a serialized memory image (vm.mem), the VMM's device and vCPU state (vm.state), and a rootfs disk. And both clone that snapshot with copy-on-write rather than duplicating bytes:

  • Memory: the snapshot's vm.mem is mapped MAP_PRIVATE. The guest's RAM is backed by the shared image, nothing is eagerly copied, and pages fault in lazily. A read fault shares the page; a write fault copies just that one 4 KiB page private to this guest.
  • Disk: the rootfs is cloned with an XFS reflink (or dm-snapshot) — a new file pointing at the same data extents in O(metadata). Blocks stay shared until one side writes, then only the affected blocks are copied for the writer.
  • Resume: neither reboots. The vCPUs resume against the mapped memory and device state, so there is no init, no kernel handshake — the guest picks up exactly where the snapshot left it.

So both operations are "map a memory image and a disk copy-on-write, then resume." The only thing that differs is which snapshot you point at — and that difference is the whole story.

The semantic difference: template-fresh vs live-branch

Snapshot-restore points at the generic, baked template snapshot. The first time a template is ever spawned, the agent does a real cold boot (~3s) of a clean guest and captures a snapshot of it in its ready state. Every create after that restores that same frozen baseline. So every restored sandbox is deterministic and identical: same processes, same clean filesystem, same starting memory. It has no memory of any previous sandbox. This is exactly what you want when each unit of work must start from a known-good, uncontaminated baseline.

Fork points at a snapshot of a specific running sandbox — the one you have been working in. It captures that machine's live memory and disk at the instant you fork: the loaded model, the authenticated session, the cloned repo, the computed dataset, the half-finished migration. The child inherits all of it and then diverges independently. This is what you want when the setup was expensive and you need several branches that all start from that warmed-up point rather than redoing the work N times.

Restore gives you a machine that has never done anything. Fork gives you a machine that has already done the expensive part. The choice is entirely about whether you want the warm state or want to escape it.

Restore vs fork, side by side

  • Starting state — Snapshot-restore: the generic template baseline, identical every time. Fork: a specific running VM's live memory + disk at the fork instant.
  • Determinism — Snapshot-restore: fully deterministic; every sandbox is byte-identical at t=0. Fork: inherits whatever the parent had done, warm caches and all.
  • What it's for — Snapshot-restore: fresh, isolated, per-request/per-tenant sandboxes. Fork: branching one warmed environment into N divergent children without re-running setup.
  • Latency — Snapshot-restore: ~49ms and 179ms p50, ~203ms p99. Fork: same-host 400–750ms, cross-host 1.2–3.5s.
  • Placement — Snapshot-restore: any agent that has the seed; the scheduler load-spreads. Fork: defaults to the parent's host so memory and rootfs are local for reflink.
  • Setup cost — Snapshot-restore: none; the template already carries the baked state. Fork: you pay to build the warm state once in the parent, then branches are cheap.

Latency and placement, and why they differ

Restore is faster than fork, and the reason is placement, not the amount of RAM. A restore uses a template seed that every agent already holds locally, so the scheduler is free to place a create on any agent — it load-spreads by free CPU and memory — and the restore is a local memory-map plus a local reflink and a resume. That is the ~49ms/179ms p50, ~203ms p99 path (versus ~3s for the one-time first cold boot before a template's snapshot exists).

A fork's data is not generic — it is one specific parent's live memory and disk. Copy-on-write can only share pages and extents that physically exist on the host, and a reflink only works within one filesystem. So the fast path is a same-host fork (400–750ms): the parent's RAM is already resident and the rootfs reflinks locally, no data moves. A cross-host fork (1.2–3.5s) has to ship the vm.mem and disk artifacts over the network before the target can restore them. Because of this, the scheduler defaults a fork to the parent's host; cross-host is opt-in for when you specifically need a branch elsewhere (capacity, isolation, region). Keeping a fan-out on the parent's host is both faster and lets every branch share the parent's page cache.

Fork shares unmodified state — it does not isolate you from the parent's bugs. If the parent is wedged or mid-failure at the fork instant, every child inherits that exact state. Restore, by contrast, always hands you the clean template baseline. If you want a guaranteed-good branch point, restore-fresh or take a snapshot from a known-good checkpoint before forking.

The decision rules

Use this list to pick. The question that decides it is always the same: do you need the warm state, or do you need a clean start?

  1. Each unit of work must start clean and identical → snapshot-restore. Test isolation, per-request code interpreters, per-tenant sandboxes, autograders, untrusted-code execution. You want determinism and no contamination between runs, and a fresh create is 49ms anyway.
  2. You built up expensive state once and now need N branches from it → fork. Tree-of-thought / best-of-N agent exploration, Monte-Carlo fan-out from a primed simulation, a loaded model or filled KV cache you want several completions from, an authenticated/warmed session you want to try several actions against.
  3. Setup is cheap or there is no shared context → snapshot-restore. If each job would only re-clone a small repo or start from the template as-is, N fresh creates are simpler than forking and give a guaranteed-clean baseline.
  4. Setup is expensive and shared → fork. If every branch would otherwise re-download a dataset, re-index a codebase, re-warm a cache, or replay a long boot, fork once-warmed and let branches share it via copy-on-write.
  5. You need branches to diverge, be evaluated, and mostly discarded → fork. The losers barely cost anything because copy-on-write meant they never allocated the untouched pages they inherited. Keep the winner, kill the rest.
  6. You need long-lived, almost-fully-divergent machines → snapshot-restore. The cheaper a fork stays, the more it shares; if branches will rewrite most of their state and run for a long time, the CoW savings erode and a clean create is cleaner.

Snapshot-restore: fresh sandbox per request (Python)

The restore case is the plain create. Each call hands you a clean, template-identical machine — perfect for isolating untrusted or per-request work. Set PANDASTACK_API_KEY in your environment first; install with pip install pandastack.

from pandastack import Sandbox

# Each request gets a FRESH, deterministic sandbox restored from the
# baked template snapshot (p50 179ms). No state carries over between
# requests -- clean isolation for untrusted / per-tenant code.
def run_untrusted(user_code: str) -> str:
    sbx = Sandbox.create(template="code-interpreter", ttl_seconds=120)
    try:
        sbx.filesystem.write("/work/main.py", user_code)
        result = sbx.exec("cd /work && python main.py", timeout_seconds=30)
        return result.stdout if result.exit_code == 0 else result.stderr
    finally:
        sbx.kill()   # nothing to preserve -- the next request restores fresh

for snippet in incoming_requests:
    print(run_untrusted(snippet))   # every run starts from the same clean baseline

Fork: warm once, then branch into N (Python)

The fork case builds expensive state once, then fans out. Each child MAP_PRIVATE-maps the parent's RAM and reflinks its disk, so all the branches share the warm setup and only diverge as they write.

from pandastack import Sandbox

# 1. Pay the expensive setup ONCE in a parent sandbox.
parent = Sandbox.create(template="base", ttl_seconds=3600)
parent.exec("git clone --depth 1 https://github.com/acme/service /work")
parent.exec("cd /work && npm ci")            # warm node_modules + build cache
parent.filesystem.write("/work/goal.md", "make the failing test pass")

# 2. Fork the LIVE machine into N branches. Same-host fork is 400-750ms
#    each and copies zero bytes up front -- each child inherits the
#    cloned repo, installed deps, and warm caches via copy-on-write.
candidates = [
    "./fix --strategy=retry-with-backoff",
    "./fix --strategy=null-guard",
    "./fix --strategy=widen-types",
    "./fix --strategy=reorder-init",
]
winners = []
for patch in candidates:
    child = parent.fork()                     # branch shares parent state (CoW)
    child.exec(f"cd /work && {patch}")
    result = child.exec("cd /work && npm test", timeout_seconds=300)
    if result.exit_code == 0:
        winners.append(child)                 # keep the passing branch
    else:
        child.kill()                          # losers barely cost anything

print("passing branches:", [w.id for w in winners])
parent.kill()   # the pristine warm state can be re-forked anytime before this

The shape is the tell. Restore is called once per independent unit of work and each call is self-contained. Fork is called after a parent has been warmed, and the branches are meaningful only because they inherit that warmth. If your loop does the expensive setup inside the loop, you probably want to hoist it out and fork; if there is no expensive setup to share, you want plain restores.

When it is genuinely either

Sometimes both work and the pick is a tradeoff rather than a rule. If the shared setup is modest — a small repo clone, a couple of pip installs — a same-host fork saves you 400–750ms of redo per branch, but N fresh restores at 179ms each are simpler, load-spread across agents rather than piling onto the parent's host, and give a guaranteed-clean baseline. Fork wins decisively when the setup is heavy (multi-gigabyte datasets, indexed codebases, loaded models, long warm-ups) and the branches are short-lived and mostly discarded. Restore wins decisively when isolation and determinism matter more than reusing state. When in doubt, ask whether the state you would inherit is an asset you paid for or a liability you want gone — that answers it.

One capacity note that occasionally tips the decision: a single agent pre-allocates 16,384 /30 subnets, so neither restore nor same-host fork is bottlenecked by network slots — the binding constraint is host memory and CPU. That means a large same-host fork fan-out competes for the parent host's RAM specifically, whereas restores spread across the fleet. For very wide fan-outs, that placement difference is worth weighing.

The mental model that holds up: restore and fork are the same copy-on-write snapshot primitive aimed at two different starting points. Restore aims it at the clean template and gives you a fresh machine; fork aims it at a live machine and gives you a branch of it. Pick restore when each unit of work should begin from a known-good, identical baseline, and fork when several units of work should begin from one hard-won warm state. PandaStack's core is open source under Apache-2.0, so you can run the agent on your own KVM hosts and time both paths yourself. For the deeper mechanics, see /blog/snapshot-and-fork-explained and /blog/copy-on-write-memory-fork-explained.

Frequently asked questions

What is the difference between snapshot-restore and fork?

Snapshot-restore boots a fresh VM from the generic baked template snapshot, so every sandbox starts from the same clean, deterministic baseline with no memory of any previous sandbox. Fork clones a specific running VM's live memory and disk at the instant you fork, so the child inherits everything that VM had done — loaded models, warm caches, cloned repos — and then diverges. Both use the same Firecracker snapshot plus copy-on-write mechanism; they differ only in which snapshot they point at: the generic template baseline versus a specific live machine.

When should I use snapshot-restore instead of fork?

Use snapshot-restore when each unit of work must start clean and identical: test isolation, per-request code interpreters, per-tenant sandboxes, autograders, and untrusted-code execution. It gives you determinism and no contamination between runs, and a fresh create lands at a p50 of 179ms (~49ms for the restore itself) anyway. Reach for restore whenever the state you would inherit from a fork is a liability rather than an asset.

When should I use fork instead of snapshot-restore?

Use fork when you have built up expensive state once and need several branches from it: tree-of-thought or best-of-N agent exploration, Monte-Carlo fan-out from a primed simulation, several completions from a loaded model or filled KV cache, or trying multiple actions against one authenticated/warmed session. Fork lets N children share that warm state via copy-on-write, so you avoid replaying the costly setup per branch. A same-host fork is 400–750ms and the discarded branches barely cost anything.

Why is fork slower than snapshot-restore, and why does host placement matter?

Restore uses a generic template seed that every agent already holds locally, so the scheduler can place the create on any agent and restore from a local file (~49ms/179ms p50). A fork's data is one specific parent's live memory and disk, and copy-on-write can only share pages and extents physically present on a host — a reflink only works within one filesystem. So a same-host fork (400–750ms) shares the parent's resident RAM and reflinks locally, while a cross-host fork (1.2–3.5s) must ship the memory and disk artifacts over the network first. The scheduler defaults a fork to the parent's host for this reason; cross-host is opt-in.

Can I fork instead of restoring to make creates faster?

No — that inverts the tradeoff. Restore is the faster and cleaner path (179ms p50, load-spread across the fleet, guaranteed-clean baseline). Fork is slower (400–750ms same-host) and pins to the parent's host, and its value is inheriting warm state, not speed. Fork only wins when the setup you would otherwise redo is expensive enough that reusing it beats a clean 179ms create. If there is no expensive shared state to inherit, plain restores are simpler, faster, and better isolated.

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.