The traditional “one Postgres container per test” approach costs roughly 2 seconds per test run: ~300 ms for the container to start, ~1.5 s forDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/deeplethe/forkd/llms.txt
Use this file to discover all available pages before exploring further.
initdb, and ~200 ms for the postmaster to begin accepting connections. With forkd, initdb runs once when the parent snapshot is built. Every fork inherits the post-init, postmaster-running state via copy-on-write memory. The result is ~10 ms per child instead of ~2 s — roughly a 200× speedup at the fixture level, with full KVM isolation between each test’s database.
The cost breakdown
| Step | Cold-start cost (per test) | forkd cost (per fork) |
|---|---|---|
| Container / VM start | ~300 ms | — |
initdb | ~1 500 ms | — (runs once at build) |
| Postmaster ready-to-accept | ~200 ms | ~10 ms |
| Total | ~2 000 ms | ~10 ms |
Build the snapshot
Build the parent rootfs
postgres:16, runs initdb, seeds the forkd_test database, and launches the postmaster. The entire post-init state is baked into parent.ext4. Rootfs: ~500 MB.Custom credentials can be baked at build time:What the build produces
Every fork of thepgfix snapshot starts with:
- Postgres 16 postmaster already running and accepting connections
- Database
forkd_test(or your custom$PG_DATABASE) already initialized - User
forkd/ passwordforkdwith trust auth on0.0.0.0/0(safe — Postgres is only reachable inside the child’s private netns) - Any SQL files placed in
/docker-entrypoint-initdb.d/during the build run automatically — the recommended way to pre-seed a schema
The fork-per-test demo
Thedemo.py script fans out 10 isolated Postgres children, runs a CREATE TABLE / INSERT / SELECT sequence in each, and proves the databases are independent by verifying each child sees only its own rows.
Python SDK usage
Performance numbers
Measured on Linux 6.14 / 20 vCPU / 30 GiB / KVM with a 300 MB rootfs. “Ready-to-query” includes the round-trip needed topsql -c "SELECT 1" from outside the netns.
| N | Wall-clock (fork + ready-to-query) | Per-child |
|---|---|---|
| 1 | ~50 ms | 50 ms |
| 10 | ~80 ms | 8 ms |
| 50 | ~250 ms | 5 ms |
| 100 | ~500 ms | 5 ms |
Isolation guarantees
Each child is a separate Firecracker microVM with its own KVM address space. From Postgres’s perspective:/dev/shmis per-child VM — shared memory buffers don’t collide between tests- WAL is per-child — transaction logs diverge independently
- Postmaster PID is the same in all children at fork time (inherited from the parent snapshot), but PIDs are per-PID-namespace, so there is no conflict
- Schema / data writes diverge per child via mmap CoW — one test writing rows never affects a sibling
Pre-loading a schema
To ship a schema into every child automatically, place migration SQL in/docker-entrypoint-initdb.d/ before taking the snapshot:
/docker-entrypoint-initdb.d/ run during the first-boot hook before the snapshot is taken.
Use cases
CI integration tests
Hundreds of DB-touching integration tests in parallel — each test gets its own Postgres with the same schema, no cleanup required between runs.
Schema migration testing
Boot the parent at the previous migration version, fork N children, apply a migration candidate in each, compare results. No shared state, no ordering dependencies.
Parallel CI workers
Replace per-worker Docker Postgres containers. Skip per-worker container cold-start and
initdb; pay only the ~5–10 ms fork cost instead.Fuzz testing
Each fuzz run gets a clean database at the parent’s seeded state — no teardown, no re-init, no accumulated corruption from prior runs.
This recipe is designed for ephemeral test fixtures. Postgres state is lost when the child VM is killed. For persistent storage across forks, attach a separate volume after the fork — that pattern is not yet included in this recipe.