Copy-on-Write File Systems

Journaling keeps the old overwrite-in-place layout and bolts a log on the side to survive crashes — paying the double-write tax. Copy-on-write (COW) file systems — ZFS (Sun, 2005) and btrfs (Linux) are the famous ones, with Network Appliance's WAFL the pioneer — make a cleaner bargain: never overwrite live data at all. To change a block, write a new copy elsewhere, thread a new path up to the root, and then flip a single pointer to make the new version the official one. The old version is left entirely intact.

This one decision cascades into an astonishing set of benefits. Crash consistency becomes free — the old tree is always a valid file system, so a crash mid-update simply leaves you on the old root, no journal, no fsck. Snapshots become nearly free — the old tree you were going to abandon is a snapshot, sharing every unchanged block. And because each pointer can carry a checksum of what it points to, the file system can detect and even repair silent disk corruption. COW is the design where consistency, snapshots, and integrity all fall out of a single structural rule.

The file system is a tree; update it bottom-up

Picture the whole file system as one big tree of blocks: a root block points at interior blocks, which point at more, down to the leaf data blocks. To modify a leaf, COW does not touch it in place. Instead it walks the change bottom-up:

Until the root flips, a reader sees the entire old tree — complete and consistent. After it flips, readers see the entire new tree — complete and consistent. There is no in-between state a crash could expose, because the only thing that changed atomically is one pointer. This is why COW file systems need no journal: the layout itself is always consistent by construction.

Watch the root flip

Follow the update of a single leaf. Only the blocks on the path from that leaf to the root are copied; every other subtree is shared between the old and new trees, untouched. When the root flips, the old root does not become garbage — it becomes a perfectly usable snapshot.

Snapshots and clones: sharing is the whole trick

Because a COW update already leaves the old tree intact, a snapshot costs almost nothing: just keep the old root and refuse to free the blocks it reaches. The snapshot and the live file system share every block they have in common; only blocks that are subsequently changed get new copies, and the changed block's old version stays alive as long as some snapshot references it. This is why you can snapshot a multi-terabyte ZFS pool in milliseconds and it consumes essentially zero extra space until data diverges.

A clone is a writable snapshot — start from a shared tree and let both sides COW their own changes. Databases, container images (each layer a clone), and "time-machine" backups all ride on this. The bookkeeping is reference counting (btrfs) or birth times (ZFS): a block may be freed only when no live tree or snapshot still points at it.

// Snapshots share unchanged blocks; only the path from a changed leaf to the root is copied (COW). // Tree: root -> {L, R}; L -> {a, b}; R -> {c, d}. let live = new Set<string>(["root", "L", "R", "a", "b", "c", "d"]); console.log(`live tree: ${live.size} blocks`); // Take a snapshot: keep the current root; share ALL blocks, copy NOTHING. const snapshot = new Set(live); console.log(`snapshot taken -> 0 new blocks written, all ${snapshot.size} shared`); // Modify leaf d. COW rewrites only the path d -> R -> root. const path = ["d", "R", "root"]; const copied = path.map((b) => b + "′"); live = new Set(["root′", "L", "R′", "a", "b", "c", "d′"]); console.log(`modify leaf d -> copied ${copied.length} blocks along the path: ${copied.join(", ")}`); const shared = [...live].filter((b) => snapshot.has(b)); console.log(`still shared with the snapshot (free): ${shared.join(", ")}`); const onDisk = new Set([...live, ...snapshot]); console.log(`unique blocks on disk: ${onDisk.size} (a full copy would need ${live.size + snapshot.size})`);

Checksums: end-to-end integrity

COW hands you integrity almost for free too. Since every block already gets a fresh parent pointer on update, ZFS and btrfs store the block's checksum in its parent rather than beside the data. So when you read a block, you validate it against a checksum that lives somewhere else on the disk — a genuine end-to-end check that catches not just bit-rot but misdirected writes, phantom writes, and a disk returning the wrong block entirely. With a redundant copy (a mirror or RAID-Z), a failed checksum triggers self-healing: read the good copy, hand the caller correct data, and rewrite the bad block. This is why ZFS is trusted for archival storage — it does not just survive crashes, it detects the disk lying.

Because the old versions are freed once nothing references them. When you update a leaf and flip the root, the old leaf, old parent, and old root become garbage unless a snapshot still points at them — at which point a background reclaimer (or reference-count drop) returns their space to the free pool. So COW is not "keep everything forever"; it is "keep the old version alive exactly as long as some tree still needs it." The cost you do pay is write amplification: changing one leaf forces you to rewrite every block on the path to the root (a handful of blocks for one data change), and over time all this out-of-place writing fragments the disk, so a file's blocks scatter and sequential reads slow down. ZFS fights this with large caches (the ARC) and clever allocation; btrfs offers explicit defragmentation. It is the same space-and-fragmentation trade that log-structured file systems make — COW is, in a sense, LFS's tree-shaped cousin.

The seductive picture of COW is "just write the new block somewhere else." But the parent pointer must change to find that new block, which means the parent is rewritten, which changes its parent, all the way up. So a one-byte change to a leaf in a tree of height h triggers h+1 block writes (the leaf plus every ancestor up to and including the new root). This write amplification is the price of never overwriting in place, and it is why COW file systems batch many changes into a single root-flip (amortising the path-to-root cost over lots of updates) and why deep trees are more expensive to modify than shallow ones. Never model a COW write as a single I/O — count the path.

Where this sits

COW, journaling, and log-structured design are three answers to the one crash-consistency question, and they share DNA: all three refuse to trust an in-place overwrite as atomic. But even a perfectly consistent COW file system still faces the last problem in this module — when \texttt{write()} returns, the new root may still be sitting in a volatile cache, not on stable media. Guaranteeing "it is really, durably on disk now" is the job of fsync and write barriers, next.