The Zygote Pattern: Fork One Warm Snapshot Into Thousands of MicroVMs
Here's the density problem in one sentence: if a warmed-up Python or Node microVM needs 1.5 GB of RAM once the interpreter, standard library, and your framework are all resident, then a thousand of them naively cost 1.5 TB — and most of that memory is identical, byte-for-byte, across every VM. The interpreter binary is the same. The stdlib pages are the same. The framework you imported is the same. Only a sliver of each VM's memory is actually unique to that VM. The zygote pattern is how you stop paying for the duplicate part.
The idea is borrowed from Android. Android boots one process, the zygote, that preloads the entire runtime and every common library, then forks it to launch each app. The children inherit the zygote's already-initialized memory and share it copy-on-write, so app launch is fast and the shared runtime is paid for once. A microVM zygote does the same thing one layer down: bake a snapshot of a fully-initialized guest, then fork that snapshot many times. The zygote is the one VM that does all the boring imports so its thousand children don't have to.
What copy-on-write actually buys you
A Firecracker snapshot is two big things plus some state: a memory file (`vm.mem`) that is a raw dump of the guest's RAM, and the guest's device/CPU state (`vm.state`), alongside the rootfs disk. Restoring maps that memory file into the new VM's address space. The trick that makes forks cheap is how it's mapped: with `MAP_PRIVATE`, the kernel maps the parent's memory pages into the child read-only and shared, and only makes a private copy of a page the instant the child writes to it. Read a page, it's shared. Write a page, the kernel traps the fault, copies that one 4 KiB page, and points the child at its private copy. Everything untouched stays shared.
So the cost of a fork isn't N× the guest's RAM — it's the size of the pages each child actually dirties. Read-only memory — the interpreter's code, mapped shared libraries, an imported framework's bytecode, a model's weights loaded read-only — never gets written after the snapshot, so it's shared across every fork for the whole life of the VM. The write-heavy part (request buffers, per-user state, a growing heap) diverges, and that's the only memory you pay N times for.
The key move: import the heavy stuff before you snapshot
This is the part people miss. Copy-on-write sharing only helps for pages that exist at snapshot time and don't change afterward. If your framework is imported lazily by each child after the fork, then each child faults its bytecode in independently and dirties its own copy — no sharing. If instead you import everything in the zygote, before you take the snapshot, those pages are baked into `vm.mem` once and every fork inherits them shared. Warm state that's frozen into the snapshot is free for every child; warm state that's built after the fork is paid for per child.
So the recipe for a good zygote is: create a sandbox, do every expensive, deterministic bit of initialization you can — import the big library, warm the import cache, load read-only reference data or model weights, pre-JIT hot paths — then snapshot. The heavier and more shared that warm state, the better the density math gets.
from pandastack import Sandbox
# 1) Build the zygote: a VM with the heavy framework fully imported.
zygote = Sandbox.create(template="code-interpreter", persistent=True)
# Preload everything the children will need, INTO memory, before snapshotting.
# Every page these imports touch gets frozen into the snapshot and shared CoW.
warm = """
import numpy, pandas, sklearn # heavy imports: pull bytecode + .so into RAM
import transformers # ~hundreds of MB of import state
_ = pandas.DataFrame({'x': range(1000)}) # touch the hot code paths once
print('zygote warm: modules resident')
"""
r = zygote.exec(f"python3 -c {warm!r}", timeout_seconds=120)
assert r.exit_code == 0, r.stderr
# 2) Freeze the warm state. This snapshot's vm.mem now contains the imported
# interpreter + framework pages -- paid for exactly once.
snap = zygote.snapshot()
print("zygote snapshot id:", snap.id)That `snapshot()` call is the whole payoff. Its memory file holds the interpreter, the C extensions, and the imported framework, all resident and warm. Nothing after this point re-does that work — the children restore from here.
Forking the zygote many times
Now spawn the children. Each `fork()` restores from the zygote's snapshot and maps its memory `MAP_PRIVATE`, so the child comes up with the framework already imported — no cold `import`, no re-warming — and shares the parent's read-only pages until it writes. A same-host fork lands in roughly 400–750ms (local reflink of the rootfs plus a memory restore); a cross-host fork is 1.2–3.5s because the memory file has to come across the network first. The child is a full, independent microVM: its own kernel, its own writable heap, its own network namespace. It just started life holding the zygote's warm memory instead of a blank slate.
from pandastack import Sandbox
# Fork the warm zygote into many children. Same-host forks share the parent's
# read-only pages copy-on-write, so the imported framework is resident in each
# child WITHOUT re-importing and WITHOUT duplicating those pages in RAM.
children = []
for i in range(200):
child = zygote.fork() # ~400-750ms same-host; framework already warm
children.append(child)
# Each child runs immediately -- numpy/pandas/transformers already imported.
for child in children[:3]:
out = child.exec(
"python3 -c 'import numpy; print(numpy.arange(5).sum())'",
timeout_seconds=10,
)
print(child.id, out.stdout.strip()) # -> 10 (no import lag)
# The read-only interpreter + framework pages are shared across all 200 VMs.
# Only the pages each child WRITES (its heap, request state) cost extra RAM.
for child in children:
child.kill()
zygote.kill()If you're fanning out one task across many isolated attempts — best-of-N sampling, parallel test shards, per-user interpreter sessions — this is the shape you want. Fork the warm zygote per unit of work, let each child diverge on its own writes, kill it when done. The forks are cheap in both time and memory precisely because they start from shared warm state.
Where copy-on-write breaks down
Copy-on-write is a bet that children mostly read the shared pages and write little. When that bet is wrong, the sharing evaporates. A write-heavy workload — one that mutates large arrays in place, allocates and churns a big heap, or rewrites much of the framework's own data structures — dirties page after page, and each dirtied page becomes a private copy. In the limit, a child that touches every page it inherited ends up with a full private copy of the guest's memory, and you're back to paying N× RAM. The density win is proportional to how much of the working set stays read-only after the fork.
Two practical corollaries. First, keep the genuinely shared, read-only things — interpreter, stdlib, framework bytecode, reference data, read-only model weights — in the snapshot, and keep per-child mutable state small. Second, remember there are two copy-on-write systems in play: memory CoW (the `MAP_PRIVATE` page sharing above) and disk CoW (reflink/dm-snapshot on the rootfs, where the child's disk shares the parent's blocks until it writes). Both amplify density; both erode under heavy writes. A guest that rewrites gigabytes on disk diverges its rootfs just like a guest that dirties gigabytes of RAM diverges its memory.
The page-cache angle: sharing across VMs on one host
There's a second, related sharing effect at the host level. The rootfs disks behind many sandboxes on one host are copy-on-write clones of the same template image, so when the guest reads a shared block, the host reads it from disk once and keeps it in the host page cache — served to every VM that reads the same block, with one physical read and one cached copy. That's why cold-booting each VM from a shared template still benefits from locality even without memory forking. The zygote pattern stacks memory CoW on top of that disk-and-page-cache sharing: the disk blocks are shared through reflink and the host cache, and the warm in-RAM state is shared through `MAP_PRIVATE`. Two layers of "pay once, share to many."
For a deeper look at the disk side, see how copy-on-write rootfs works and how page-cache sharing raises microVM density.
Cold boot each vs warm pool vs zygote fork
Three ways to get from "I need many warmed VMs" to actually having them, and what each costs:
- Startup latency — Cold-boot each: full boot + framework import every time (seconds; ~3s first cold boot on PandaStack). Warm pool: instant if a VM is free, else you're cold-booting anyway. Zygote fork: ~400–750ms same-host, and the framework is already imported.
- Memory cost for N VMs — Cold-boot each: ~N× the full warm footprint, nothing shared. Warm pool: N× full footprint held idle, whether or not they're in use. Zygote fork: one warm footprint shared read-only + N small write-deltas (copy-on-write).
- Idle waste — Cold-boot each: none (you only pay when running). Warm pool: high — you keep idle VMs resident to hide boot latency. Zygote fork: none beyond the one zygote; children are created on demand.
- Redundant work — Cold-boot each: every VM re-imports the framework from scratch. Warm pool: pre-warmed once per pooled VM. Zygote fork: imported once in the zygote, inherited by every child.
- Best fit — Cold-boot each: rare, unpredictable, fully-diverging workloads. Warm pool: steady traffic where you can size the pool. Zygote fork: bursty fan-out of many short-lived VMs that share a big warm base (agents, best-of-N, per-request interpreters).
- Where it breaks — Cold-boot each: latency-sensitive or high-QPS paths. Warm pool: idle cost and cold-start cliffs when the pool drains. Zygote fork: write-heavy children that dirty most of the shared pages.
The honest framing: a warm pool hides latency by keeping whole VMs idle, which costs memory whether or not they're used. Zygote fork hides both the latency and the memory by sharing one warm base copy-on-write and materializing children on demand. It wins hardest exactly where you'd otherwise want a big warm pool — many short-lived VMs, all built on the same heavy runtime, appearing in bursts.
PandaStack builds on this directly: every create restores a baked template snapshot (p50 ~49ms restore, ~179ms end-to-end create), and `fork()` clones a running sandbox's memory and disk copy-on-write. Bake your warm state into a template or a snapshot once, fork it per unit of work, and let the kernel's page-fault handler do the accounting. For the mechanics underneath, see snapshot and fork explained.
Frequently asked questions
What is the zygote pattern for microVMs?
It's borrowed from Android's zygote process: boot and fully warm up one microVM — import the heavy framework, load read-only reference data, warm the caches — then snapshot it and fork that snapshot many times. The forked children inherit the parent's already-initialized memory and share its read-only pages copy-on-write, so the expensive warm state is paid for once and shared across every VM instead of being rebuilt in each one.
How does forking a snapshot save memory?
Firecracker maps the snapshot's memory file into each child with MAP_PRIVATE, so the child shares the parent's pages read-only and the kernel only makes a private copy of a page the moment the child writes to it. Read-only pages — interpreter, stdlib, imported framework, read-only model weights — are never written after the snapshot, so they stay shared across every fork. You only pay extra RAM for the pages each child actually dirties, not the full guest footprint times N.
Why import the framework before taking the snapshot?
Copy-on-write only shares pages that exist at snapshot time and don't change afterward. If each child imports the framework after the fork, it faults in and dirties its own private copy — no sharing. If you import it in the zygote before snapshotting, those pages are frozen into the memory file once and every fork inherits them shared. Warm state baked into the snapshot is free for every child; warm state built after the fork is paid for per child.
When does copy-on-write sharing stop helping?
When children write heavily. CoW is a bet that forks mostly read the shared pages. A write-heavy workload that mutates large arrays in place or churns a big heap dirties page after page, and each dirtied page becomes a private copy — in the limit a child that touches everything ends up with a full private copy of the guest's memory. The density win is proportional to how much of the working set stays read-only after the fork, so keep the shared base read-only and per-child mutable state small.
How fast is forking a warm snapshot?
On PandaStack a same-host fork is roughly 400–750ms (a local reflink of the rootfs plus a memory restore), and a cross-host fork is 1.2–3.5s because the memory file has to transfer across the network first. Either way the child comes up with the framework already imported, so there's no cold-import lag on top — the warm state was inherited from the zygote's snapshot rather than rebuilt.
49ms p50 cold start. Fork, snapshot, and scale to zero.