Introduction

When a program calls fork(), the operating system creates a child process that is an exact copy of the parent. In the 1970s that meant physically duplicating every page of memory — instantly expensive, even if the child immediately replaced itself with a new program via exec().

Copy-on-write (COW) changed the game with a beautifully lazy idea: instead of copying, let both parent and child point at the same physical pages and mark them all read-only. Nothing is duplicated yet. The moment either process tries to write to one of those pages, the hardware raises a fault, the kernel steps in, makes a private copy of just that page, and lets the write proceed. Pages that are never written are never copied at all.

The result is that fork() goes from O(n)O(n) work — proportional to the size of the process — to roughly O(1)O(1) work plus a small per-write overhead paid lazily. For programs that fork only to exec a new binary right away, the total copy cost approaches zero.

Fork and Write

The demo below simulates a process with several memory pages. Each page starts shared between parent and child after a fork. Click Fork to create the child, then click any page to write to it and watch copy-on-write split it into two private copies.

<!-- {{c_html_intro}} -->
<p class="hint">{{hint_para}}</p>
<div class="arena" id="arena">
  <div class="proc-col" id="col-parent">
    <div class="proc-label">{{label_parent}}</div>
    <div class="pages" id="pages-parent"></div>
  </div>
  <div class="proc-col hidden" id="col-child">
    <div class="proc-label">{{label_child}}</div>
    <div class="pages" id="pages-child"></div>
  </div>
</div>
<div class="status" id="status">{{status_idle}}</div>
<div class="btns">
  <button id="btn-fork" type="button">{{btn_fork}}</button>
  <button id="btn-reset" type="button" class="ghost">{{btn_reset}}</button>
</div>
<div class="legend">
  <span class="badge shared">S</span> {{legend_shared}}
  <span class="badge private-p">P</span> {{legend_parent_private}}
  <span class="badge private-c">C</span> {{legend_child_private}}
</div>
/* {{c_css_intro}} */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.arena { display: flex; gap: 1.5rem; margin-bottom: .6rem; }
.proc-col { flex: 1; }
.proc-label { font-size: .8rem; font-weight: 700; text-transform: uppercase;
              letter-spacing: .06em; color: #555; margin-bottom: .4rem; }
.pages { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.page { border-radius: 8px; height: 56px; display: flex; flex-direction: column;
        align-items: center; justify-content: center; font-size: .78rem; font-weight: 600;
        cursor: pointer; border: 2px solid transparent; transition: all .15s;
        user-select: none; position: relative; }
.page .pg-name { font-size: .7rem; opacity: .7; }
.page .pg-badge { position: absolute; top: 3px; right: 5px; font-size: .65rem;
                  font-weight: 800; }
/* {{c_css_shared}} */
.page.shared { background: #dbeafe; border-color: #93c5fd; color: #1e40af; }
.page.shared:hover { background: #bfdbfe; }
/* {{c_css_private_p}} */
.page.private-p { background: #dcfce7; border-color: #86efac; color: #166534; }
/* {{c_css_private_c}} */
.page.private-c { background: #fef9c3; border-color: #fde047; color: #713f12; }
.page.cow-anim { animation: cowpop .4s ease; }
@keyframes cowpop { 0%{transform:scale(1)} 40%{transform:scale(1.18)} 100%{transform:scale(1)} }
.hidden { visibility: hidden; }
.status { font-size: .95rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; }
.status.ok { color: #166534; }
.status.info { color: #1e40af; }
.status.warn { color: #92400e; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.legend { font-size: .78rem; color: #555; display: flex; gap: .8rem; flex-wrap: wrap;
          align-items: center; }
.badge { border-radius: 4px; padding: 1px 5px; font-size: .7rem; font-weight: 800;
         display: inline-block; }
.badge.shared { background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd; }
.badge.private-p { background: #dcfce7; color: #166534; border: 1px solid #86efac; }
.badge.private-c { background: #fef9c3; color: #713f12; border: 1px solid #fde047; }
// Code not found

Notice the asymmetry. The fork itself is instant regardless of how many pages exist — no memory is copied. Each write then pays a one-time split cost for that single page. If the child only reads and never writes, the total extra memory used is zero.

The Real Cost

Copy-on-write is not free — it trades an upfront bulk copy for a stream of per-page faults paid on demand. Understanding the real costs matters when you push it hard:

  • Fork latency drops to O(1)O(1). The kernel clones the page-table entries and marks them read-only. This is proportional to the number of page-table entries, not the data size — and modern kernels use huge pages to keep even that small.
  • Each write triggers a page fault. The hardware detects the read-only violation, traps to the kernel, which allocates a fresh physical page, copies the 4 KB of data, updates the page table, and resumes the faulting instruction. That round-trip costs a few microseconds.
  • TLB shootdowns multiply the cost in multicore systems. When a page-table entry changes on one CPU, other CPUs caching that entry must be notified via an inter-processor interrupt. With many cores all writing different pages simultaneously, shootdown storms can dominate.
  • Worst case: write-heavy workloads. A process that forks and then writes every page ends up paying the original copy cost plus the fault overhead for each page. Redis, for example, triggers this pattern during its background save — the parent keeps serving writes while the child persists a snapshot, and every dirty page doubles in memory.

The key insight is that COW moves cost from time of fork to time of first write. For scheduling or exec-heavy workflows this is a massive win. For copy-heavy ones it is a wash at best.

Where It Matters

The "share until you write" idea shows up everywhere a copy might never need to diverge:

  • OS process forking: every Unix-like OS uses COW in fork(). The shell spawns a child that execs a new binary without copying the shell's entire heap — the copy cost is zero.
  • Persistent data structures: functional languages like Clojure and Haskell use structural sharing to give you an "updated" collection that reuses most of the old one's nodes — the same COW principle applied in software rather than hardware.
  • Container and VM snapshots: Docker layers and virtual machine snapshots share disk blocks across images. A new container starts from a read-only base layer; blocks are copied to a writable layer only when written.
  • Database checkpointing: SQLite, PostgreSQL (MVCC), and Redis all use COW semantics during snapshotting. A background writer sees a stable view of the data while foreground writers create private copies of modified pages.
  • Git object storage: git's objects are immutable content-addressed blobs. Branching is free because no objects are duplicated — only new commits and trees point to the modified objects, sharing the rest.

Understand copy-on-write and you have the key to why program synthesis in functional languages is cheap, why containers start in milliseconds, and why a Redis save does not block your reads.

Conclusion

Copy-on-write is a masterclass in lazy evaluation applied to memory: why do work now when you can defer it to the moment it is actually needed — or skip it entirely if it never is?

The principle is simple enough to fit in a sentence — share pages until a write forces a split — yet it underlies process forking in every modern OS, structural sharing in functional data structures, snapshot isolation in databases, and the layered storage behind every container you have ever run.

The next time a fork() returns in microseconds despite a gigabyte heap, or a Docker image spins up in milliseconds, you are watching copy-on-write quietly doing nothing — which is exactly the point. Doing nothing, as fast as possible, until you absolutely must act.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/copy-on-write/Content licensed under CC BY-NC 4.0.