Forking a process feels like it should be proportional to the size of the process. It isn't — and that's both the reason Redis can snapshot a multi-gigabyte dataset from a single-threaded event loop, and the reason the bill for that snapshot shows up somewhere you're not looking: hundreds of milliseconds after fork() returns, one page fault at a time.
What fork() actually copies
The fork(2) man page is precise about this. The child is "an exact duplicate of the parent," and "the entire virtual address space of the parent is replicated in the child" — but replicated does not mean copied:
Under Linux, fork() is implemented using copy-on-write pages, so the only penalty that it incurs is the time and memory required to duplicate the parent's page tables, and to create a unique task structure for the child.
Both processes see identical memory. Physically, they share every page, with the shared pages mapped read-only. Only when either side writes to a page does the kernel fault, copy that one page, and give the writer its own private version. A 24 GB process forks without copying 24 GB of anything.
What it does copy is the page table. On Linux/AMD64 with 4 kB pages, mapping 24 GB takes at least one entry (8 bytes) per page:
24 GB / 4 kB × 8 bytes = 48 MB of page tableThat math comes straight from the Redis latency documentation, because for Redis this is not trivia — the fork runs on the main thread, and every client waits while the kernel builds those 48 MB of mappings.
How long that takes depends on your hypervisor more than your dataset
Redis exposes the duration of its last fork as latest_fork_usec in INFO, and the project published measurements across environments. The spread is startling:
| Environment | RSS | Fork time | Per GB |
|---|---|---|---|
| Physical machine (Xeon @ 2.27 GHz) | 6.9 GB | 62 ms | 9 ms |
| VM on VMware | 6.0 GB | 77 ms | 12.8 ms |
| EC2, modern HVM instance (Xen) | 1 GB | 10 ms | 10 ms |
| VM on Linode (Xen) | 0.9 GB | 382 ms | 424 ms |
| EC2, old instance types (Xen) | 6.1 GB | 1460 ms | 239.3 ms |
Same operation, same kernel API, and a one-to-two-order-of-magnitude difference — old Xen paravirtualized instances made page-table duplication catastrophically slow, which is why the Redis docs flatly tell EC2 users to use HVM-based instances. This is a useful general lesson: fork() cost is a property of the memory-management stack you run on, not just of your process. The persistence docs are honest about the failure mode: fork "may result in Redis stopping serving clients for some milliseconds or even for one second if the dataset is very big and the CPU performance is not great."
The deferred bill: fork took 6 ms, the spike was 300 ms
Here's the part that breaks people's mental model. In a widely-read investigation, antirez chased ~300 ms latency spikes during BGSAVE on EC2 — while latest_fork_usec showed the fork itself taking about 6 ms. The fork wasn't the spike. The spike came after, from copy-on-write doing exactly what it's designed to do.
Linux doesn't even eagerly copy all page table entries at fork. A comment in the kernel's memory-management code explains the optimization:
Don't copy ptes where a page fault will fill them correctly. Fork becomes much lighter when there are big shared or private readonly mappings.
So the work you thought happened inside fork() is smeared across the milliseconds and seconds that follow, as the parent keeps serving writes. Every write to a not-yet-copied page traps into the kernel, allocates a page, copies 4 kB, and fixes up mappings. Individually invisible; in aggregate, a latency cliff that no profiler attributes to fork() because fork() already returned.
This is also why memory usage grows during a snapshot. The Redis administration docs warn that a write-heavy instance "can use up to 2 times the memory normally used" during an RDB save or AOF rewrite, with the extra "proportional to the number of memory pages modified by writes during the saving process." Not proportional to dataset size — proportional to write locality during the snapshot window. A mostly-idle instance duplicates almost nothing; a hot one duplicates its working set.
Transparent huge pages: the 512× amplifier
Copy-on-write granularity is the page size. Enable transparent huge pages and the unit of copying becomes 2 MB instead of 4 kB: one byte written, 2 MB copied.
The Redis latency docs describe the failure sequence: fork creates two processes sharing huge pages; on a busy instance "a few event loops runs will cause commands to target a few thousand of pages, causing the copy on write of almost the whole process memory."
Antirez measured this concretely on bare metal. With jemalloc (whose allocations were being backed by AnonHugePages), the worst BGSAVE spike was 339 ms. With libc malloc, which wasn't getting huge pages, the same workload's worst spike was 21 ms. Same machine, same Redis, same fork — a 16× difference from page granularity alone. The fix that both antirez and the official docs converge on:
echo never > /sys/kernel/mm/transparent_hugepage/enabledTHP is a genuinely good optimization for TLB pressure in most long-running processes. It is specifically hostile to the fork-and-diverge pattern, because it multiplies the cost of every divergence.
Overcommit: when fork() refuses to run at all
There's one more trap. From the accounting perspective, forking a 24 GB process asks the kernel to commit another 24 GB of address space — even though CoW means almost none of it will ever be physically copied. The kernel's overcommit-accounting documentation lists "account and check on fork" as part of the heuristic mode's job, and describes the three vm.overcommit_memory modes:
- 0 (heuristic, default): "Obvious overcommits of address space are refused."
- 1 (always): never refuse, appropriate for workloads "relying on the virtual memory consisting almost entirely of zero pages" — or, equivalently, on CoW pages that will never diverge.
- 2 (strict): total commit capped at "swap + a configurable amount (default is 50%) of physical RAM," visible as
CommitLimitvsCommitted_ASin/proc/meminfo.
Under modes 0 and 2, a Redis instance using more than half your RAM can fail its background save with a fork error — not because memory ran out, but because the accounting says the worst case wouldn't fit. This is why the Redis administration docs' very first Linux setup instruction is vm.overcommit_memory = 1. You're telling the kernel: I know the worst case can't happen; the child is a snapshot reader that exits.
The pattern, and the checklist
Fork-based snapshotting is a beautiful trick — the persistence docs note that the parent "will never perform disk I/O" for RDB saves, and AOF rewriting "uses the same copy-on-write trick." The child gets a frozen, consistent view of the heap for free, with no locks and no stop-the-world pause proportional to data size. But the costs are real; they're just relocated:
- Watch
latest_fork_usec, and treat per-GB fork time as an environment property worth benchmarking. - Budget memory for CoW divergence — up to 2× under heavy writes, and size
maxmemorywith headroom. - Disable transparent huge pages on fork-heavy services, or your 4 kB copies become 2 MB copies.
- Set
vm.overcommit_memory = 1so accounting doesn't veto a fork that CoW would have made cheap.
The general lesson outlives Redis: whenever a system forks to snapshot — and this trick appears anywhere someone needs a consistent view of mutable memory — the interesting costs are not in the syscall. They're in the page faults after it, the page size multiplying them, and the accountant deciding whether to allow the fork at all.
