Benchmarks
Reproducible measurements from crucible-bench, a harness that drives a real daemon through the sdk Go package (the same typed client the CLI, TUI, and MCP server use). It reports latency distributions, fork fan-out scaling, the lazy-memory efficiency of fork, and sandbox density.

Read these as one machine's numbers, not a spec. They come from a single host, one sandbox size — enough to show the shape of the runtime, not a cross-hardware guarantee. Everything you need to re-run is at the bottom.
The filesystem under --work-base is the biggest lever
Fork clones the per-child rootfs copy-on-write. On a reflink filesystem (btrfs / XFS reflink=1) that clone is O(1) — extents are shared, nothing is copied. On ext4 (no reflink — still the most common default) it's a full byte-copy of the rootfs. Same box, same daemon, same 1 GiB rootfs; only --work-base/--chroot-base differ:
| ext4 (common default) | btrfs (reflink) | |
|---|---|---|
| Fork (warm → child), p50 | 690 ms | 207 ms — 3.3× faster |
| Fork fan-out @ 64-way | 267 ms/child · 3.7/s | 22 ms/child · 45/s — ~12× |
| Snapshot, p50 | 738 ms | 425 ms |
| 128 forks: wall time | 34.8 s | 2.1 s |
| 128 forks: host RAM | 4.9 GiB | 1.2 GiB |
If you fork a lot, put --work-base on btrfs or XFS. The rest of this page reports both, so you can see the floor (ext4) and the ceiling (reflink).
Setup
| CPU | AMD Ryzen AI 9 HX 370 (24 threads) |
| Kernel | 7.0 · Free RAM ~13–15 GiB (other workloads paused) |
| Sandbox | 1 vCPU / 512 MiB, default (python-3.12, 1 GiB) rootfs, no network |
| Daemon | v0.2.0 under jailer, CRUCIBLE_MAX_FORK=128 |
| Samples | 50 per latency op (5 warmup discarded) |
Latency
| Operation | ext4 p50 | btrfs p50 | p90 / p99 (btrfs) |
|---|---|---|---|
Exec roundtrip (true, warm) |
2.7 ms | 2.7 ms | 2.9 / 3.3 ms |
| Fork (warm snapshot → child) | 690 ms | 207 ms | 221 / 232 ms |
| Snapshot (running → on disk) | 738 ms | 425 ms | (tail to ~2.5 s) |
| Cold create (boot → agent ready) | 2.00 s | 1.87 s | 1.94 / 1.99 s |
Fork is ~9× faster than a cold create even on ext4, and ~3× faster again on reflink. That's the point of snapshot/fork: pay the ~2 s boot + setup once, then branch cheaply. Exec overhead is ~3 ms (a vsock roundtrip) and is filesystem-independent. Snapshot's btrfs p50 is ~0.4 s with occasional multi-second tails from writing the 512 MiB memory image through writeback; the median is representative.
Fork fan-out
Forking N children from one snapshot in a single call. Per-child cost falls with batch size as fixed per-fork overhead amortizes — and reflink pulls far ahead as N grows:
| Children | ext4 per-child | btrfs per-child | btrfs throughput |
|---|---|---|---|
| 1 | 909 ms | 296 ms | 3.4/s |
| 4 | 426 ms | 81 ms | 12.4/s |
| 16 | 303 ms | 38 ms | 26.6/s |
| 64 | 267 ms | 22 ms | 44.8/s |
| 128 | — | 16 ms | 61/s |
(On ext4 each child copies a full 1 GiB rootfs, so a 64-way fan-out moves ~64 GiB of disk — the throughput ceiling is the disk, not the runtime.)
Memory efficiency
The lazy-userfaultfd payoff: guest RAM is served on demand from the snapshot's memory file, so forks share pages instead of each copying 512 MiB. Forking 128 children from one warm snapshot:
| ext4 | btrfs | |
|---|---|---|
| Host RAM consumed | 4.9 GiB (~38 MiB/fork) | 1.2 GiB (~9.5 MiB/fork) |
| vs naïve 128 × 512 MiB = 64 GiB | 13× less | 54× less |
Both obliterate the naïve cost. reflink pulls ahead because ext4's per-fork rootfs copies add page-cache pressure that reflink avoids. The win also grows with fork count — the more children share a snapshot's pages, the lower the marginal cost per fork.
Density (reflink)
Forking toward a live-sandbox target on btrfs and watching free RAM:
| Live sandboxes | Free RAM |
|---|---|
| 128 | 13.4 GiB |
| 256 | 12.3 GiB |
| 384 | 10.2 GiB |
| 512 | 7.4 GiB |
512 concurrent microVMs on a laptop, with 7.4 GiB headroom left — 512 was the test's cap, not a ceiling. On reflink, density is RAM-bound (~14 MiB marginal per fork). On ext4 it's disk-bound instead — each fork writes a full ~1 GiB rootfs — so plan disk accordingly, or use reflink.
Reproduce
make bench # builds ./bin/crucible-bench
# a daemon under jailer; raise the fork cap; point --work-base at your target FS:
CRUCIBLE_MAX_FORK=128 sudo ./crucible daemon \
--firecracker-bin … --jailer-bin … --kernel … --rootfs … \
--work-base /path/to/work --chroot-base /path/to/jailer
./bin/crucible-bench \
--samples 50 --fanout 1,4,16,64,128 --mem-forks 128 --density 512 \
--json bench-results.jsonFor the ext4 numbers, point --work-base/--chroot-base at an ext4 path; for the reflink numbers, at a btrfs or XFS (reflink=1) mount. crucible-bench --help lists every knob (sandbox size, phases, profile).