---
topic: system-design
author: Crashtech Editorial
date: Aug 8, 2026 · read: 7 min
---

Copy-on-Write Snapshots

fork() shares parent memory, copying only written pages for non-blocking snapshots. Write-heavy loads can spike memory to 2x under load.

You have an 8 GB Redis dataset and you need a consistent snapshot for backup. Locking all writes while you dump to disk means 100+ milliseconds of downtime for every client. Flushing one 8 GB memcpy means burning CPU and memory bandwidth. Redis solves this differently: it tells the OS to fork the process. The child gets a read-only view of a frozen dataset and dumps it to disk while the parent immediately serves writes again. But the magic comes with a trap door.

Hero diagram of copy-on-write fork Left: Stop-the-world snapshots lock the parent and queue writes. Right: fork() shares memory pages, allowing the parent to serve traffic immediately while the child dumps.

The Naive Approach: Stop-the-World Snapshots

The straightforward way to snapshot a live dataset is to lock everything, dump the memory to disk, and unlock. It’s simple and guarantees consistency—at the cost of a hard stall.

A typical Redis instance handling 100,000 requests per second has a fresh write every few microseconds. As soon as you acquire a write lock, every new client request blocks. A single-threaded dump of 8 GB can take 5–10 seconds. Five seconds of zero throughput is a user-facing outage.

This is why naive snapshotting is a non-starter in production. You need consistency and concurrency.

The Breakthrough: fork() and Virtual Memory Sharing

Unix systems offer a primitive that seems magical at first: the fork() system call creates a child process that inherits an exact copy of the parent’s virtual memory without copying the bytes. Both processes point to the same physical RAM pages.

Here’s where the OS steps in. Every memory page on modern CPUs is protected by a page table entry that says “read-only” or “read-write.” When the parent writes to a page, the CPU’s Memory Management Unit (MMU) throws a page-fault exception. The OS kernel intercepts this fault, realizes the page is shared with the child, allocates a new physical page, copies the contents, updates the page table, and lets the write proceed on the copy.

This is copy-on-write (CoW). The name is literal: you copy the page only when someone writes to it.

The flow:

  1. Parent calls fork() → child inherits parent’s virtual address space, pointing to the same physical pages.
  2. Child enters dump loop → reads pages sequentially, writes snapshots to disk. No writes to parent memory, so no faults.
  3. Parent resumes traffic → immediately starts serving new reads and writes. Each write triggers a page fault, which creates a private copy.
  4. Child finishes dumping → exits. OS reclaims shared pages that were never written.

The result: the child sees a frozen, consistent snapshot. The parent never blocked. No full memory copy.

How Copy-on-Write Works at the Page Level

Let’s make this concrete. A typical page size is 4 KB. An 8 GB dataset is about 2 million pages.

After fork(), all 2 million pages are shared and marked read-only in both parent and child page tables. The child iterates through pages and writes them sequentially—no writes, no faults, just reads.

The parent serves traffic. When a write lands:

  1. MMU page fault → the CPU detects a write to a read-only page and raises an exception.
  2. OS kernel traps it → in the page-fault handler, the OS checks: is this page shared with a child?
  3. Allocate and duplicate → yes. OS allocates a new physical page, copies the 4 KB, and marks both pages read-write.
  4. Resume execution → the write proceeds on the new copy. The child still reads from the old page.

Only the written page is copied. All other pages stay shared. If the parent rewrites only 1% of the dataset during the dump, only 1% of pages are duplicated—a tiny overhead. The child gets a consistent view of the data as it was at fork time.

Memory mechanism diagram showing CoW page duplication Top: 24 pages all shared initially. Bottom: a write to one page triggers an MMU fault; that single page is duplicated (emerald) while others remain shared. The mechanism scales: rewrite 50% of the keyspace, and 50% of pages are copied.

The Real-World Trap: Memory Spikes Under Write Load

Here’s where the strategy falters. If the parent rewrites a significant fraction of the dataset during the dump, the OS must copy many pages, and physical memory can double.

Consider an 8 GB heap on a server with 12 GB RAM. The parent is indexed by term-expiry: every second, 100 MB of keys expire and are deleted, and 100 MB of fresh data arrives. During a 60-second BGSAVE dump:

  • The child reads all 8 GB and writes to disk at 100 MB/s (takes ~80 seconds).
  • The parent is meanwhile servicing expirations and fresh writes: ~6 GB of the heap is rewritten.
  • Each write triggers CoW: the OS duplicates the page.
  • After 80 seconds, roughly 6 GB of pages have been copied—the box now uses 8 GB (live data) + 6 GB (duplicated pages) = 14 GB.

The box has only 12 GB. It hits OOM. The instance crashes.

This is not a theoretical edge case. Redis deployments with:

  • High write throughput (e.g., cache with frequent evictions)
  • Tight memory budgets (e.g., provisioning on a per-dataset basis, with little headroom)
  • Slow I/O (e.g., network-attached storage, which slows the dump, lengthening the window for rewrites)

…are prime candidates for CoW-induced OOM.

Why You Can't Just Allocate More Pages

The issue is physical RAM, not virtual memory. Virtual address space is unlimited; the OS swaps pages to disk as needed. But fork() CoW duplicates pages in physical RAM, which is finite. If you run out of physical memory, you hit OOM even if the dataset itself is well below the heap limit.

Advertisement

When NOT to Use fork()-Based Snapshots

Copy-on-write snapshots are perfect for:

  • Low-to-moderate write throughput (e.g., a read-heavy cache, a session store, a rate limiter).
  • Datasets stable during backups (e.g., a static config server).
  • Ample spare RAM (e.g., a 50 GB heap on a 100 GB machine; a CoW spike halves available memory safely).

But they fail when:

  • Write throughput is high (e.g., a message queue, a write-heavy time-series cache, a live event log). CoW duplicates pages faster than the dump progresses, and memory grows without bound.
  • RAM is provisioned tightly (e.g., container memory limits, VPS tier-sizing). A 20% CoW overhead can trigger OOM.
  • I/O is slow (e.g., large datasets on magnetic disk or slow NAS). The longer the dump takes, the longer the window for rewrites.

Mitigation Strategies

1. Monitor and measure. Track fork() calls and page-copy latency during BGSAVE. If copies spike, reduce the write rate or allocate more RAM.

2. Use AOF instead. Append-only file (AOF) logs every command, avoiding the snapshot problem entirely at the cost of slightly higher I/O. For write-heavy workloads, AOF is often the better tradeoff.

3. Tune the eviction policy. Set maxmemory-policy to volatile-lru or volatile-ttl before BGSAVE. Evict data before the snapshot runs, reducing the in-memory size and thus the CoW spike.

4. Serialize to a replica. Run BGSAVE on a replica instance with spare RAM, keeping the primary light and responsive.

5. Allocate headroom. Reserve 50% of physical RAM as a buffer. If your dataset is 8 GB and write volume is moderate, provision 16–20 GB. The headroom absorbs CoW without OOM.

The Honest Trade-Off

fork() CoW snapshots are a triumph of operating-system engineering. They give you non-blocking consistency at nearly zero cost in the common case (low to moderate writes). But they are not free. The cost is real, it’s invisible, and it’s deferred until the moment your write rate peaks and you run out of RAM.

Understanding fork() means knowing when to trust it. For Redis deployments with stable write patterns and spare capacity, fork()-based BGSAVE is a net win—concurrent snapshots with milliseconds of blocking. For write-heavy or memory-constrained workloads, it’s a time bomb. The choice is yours, but it must be informed.

Advertisement

Frequently asked questions

How does fork() enable non-blocking snapshots?

fork() creates a child process sharing the parent's physical memory pages. The child can dump a read-only consistent view while the parent immediately resumes serving writes—no blocking required.

What's copy-on-write (CoW) at the page level?

When the parent or child writes to a shared page, the OS page-fault handler traps the write, duplicates that single page, and lets the write proceed on the copy. All other pages remain shared.

When does memory spike during BGSAVE?

If the parent process rewrites a large fraction of the dataset while the child is dumping, the OS must copy many pages. A 50% rewrite of an 8 GB heap can require 4 GB of new physical memory.

Can copy-on-write fail in production?

Yes. If the box has tight RAM (e.g., 10 GB heap on a 12 GB instance) and writes are frequent, the memory spike during BGSAVE can trigger OOM even though the post-dump size is normal.

How do you work around the memory risk?

Monitor write volume during BGSAVE; use AOF (append-only file) for write-heavy workloads; tune maxmemory-policy to evict before BGSAVE starts; or allocate spare RAM above heap size.

Sources & further reading

/* Comments */