Skip to content
Back to blog
Deep Dive 10 min read

How copy-on-write database branching actually works

The CoW clone is ~3 seconds and about 5% of branch-create time. Everything wrapped around it is the other 95%. Measurements, mechanisms, and the honest failure modes.

Vijay Gorfad
Vijay Gorfad
Founder of Sandbase
How copy-on-write database branching actually works

We’re Sandbase. Every git branch gets a real-data Postgres sandbox. It’s neat. This post is the concept explainer, not a pitch — you can build everything described here yourself.

TL;DR — Copy-on-write (CoW) branching creates a writable clone of a database by sharing every unchanged block with its parent, so clone time is O(1) in dataset size, not O(size). On our test stack, the storage-level clone of a Postgres data volume completes in ~3 seconds regardless of data volume , and an idle branch occupies 0.76 MB . The slow part is never the clone — it’s everything wrapped around it.

Why can’t I just copy the database?

You can. It’s just the worst-scaling operation in your infrastructure.

Say you have a 100 GB Postgres database and five teammates (or five AI agents) who each need an isolated copy. The naive path is pg_dump | pg_restore or pg_basebackup:

  • Bytes moved: 100 GB per copy, 500 GB total. At a realistic sustained 200 MB/s, that’s ~8.5 minutes per copy just moving bytes — and dump/restore is slower than that in practice because restore rebuilds every index.
  • Storage: 5 copies × 100 GB = 500 GB of duplicate blocks, ~99.9% of which are identical to the parent.
  • Freshness: each copy is stale the moment it finishes.

The cost is O(size × copies). Every “database branching” product — and this post covers the mechanism, not any product — exists because there’s a way to make that O(1).

Approach Clone time (100 GB) Extra storage at creation Postgres modified?
pg_dump/pg_basebackup full copy minutes–hours 100 GB No
Filesystem block CoW (ZFS/btrfs snapshot + clone) seconds, size-independent ~0 (metadata only) No
Log-structured / LSN pointer (Neon-style pageserver) sub-second branch point ~0 Yes (custom storage layer)
File-level reflink (PG18 FILE_COPY, ClickHouse hard links) sub-second on reflink FS ~0 No (needs XFS/btrfs/APFS)

What is a copy-on-write clone, physically?

A database branch is not a copy of your data; it is a promise to copy only what changes.

On a CoW filesystem like ZFS, every block on disk is referenced by pointers in a tree. A snapshot is a new root pointer into that existing tree plus a rule: blocks reachable from a snapshot are never overwritten in place. Taking a snapshot writes a few kilobytes of metadata. Nothing is copied.

A clone is a snapshot promoted to writable: a new dataset whose block tree initially points at 100% of the parent’s blocks. When the clone writes, the filesystem allocates a new block for the changed data and updates only the clone’s pointer — the parent never sees it. That’s the entire trick. A freshly cloned 100 GB database and a freshly cloned 100 TB database cost the same: metadata.

This is old, boring, proven technology. Postgres.ai’s DBLab, built on exactly this mechanism (ZFS/LVM2), documents thin clones of 10 TiB databases in under 2 seconds (source: postgres.ai Database Lab docs). ClickHouse’s ALTER TABLE ... FREEZE creates backups as hard links, which its documentation describes as near-instantaneous for the same reason: pointers, not bytes.

There are three families of the trick:

  1. Block-level CoW (ZFS, btrfs, thin-provisioned LVM, and the proprietary block stores under several cloud Postgres products): the filesystem or block device shares blocks. Unmodified Postgres runs on top and never knows.
  2. Log-structured / LSN branching (Neon’s architecture): Postgres’s write-ahead log is the storage. A branch is a pointer to a log sequence number (LSN); a custom pageserver reconstructs any page at any branch point. Maximum flexibility (branch from any point in time), at the cost of a modified storage layer and network page fetches.
  3. File-level reflinks: PostgreSQL 18’s CREATE DATABASE ... STRATEGY=FILE_COPY with file_copy_method=clone uses the filesystem’s reflink call. Published benchmarks from the PG18 cycle show a 6 GB database cloned in 212 ms versus 67 s for an ordinary copy — roughly 316× . Same-instance only, but it means the raw primitive now ships free inside vanilla Postgres.

If the primitive is free and 30 years old, why does branching feel slow in real products? Because of everything the microbenchmarks leave out.

What do the clone microbenchmarks hide?

The clone is never the slow part; everything wrapped around the clone is.

A storage-layer clone number measures snapshot-to-writable-volume. A usable database branch needs much more: a volume provisioned through CSI, a pod scheduled onto a node, Postgres started, WAL recovery replayed, and pg_isready returning true. Vendors near-universally publish the first number and users experience the second. TigerData’s Fluid Storage post, for example, reports forks in ~500–600 ms regardless of size — explicitly a block-layer measurement (source: TigerData Fluid Storage blog). Neon publishes its own compute cold-start latencies at roughly 1.8 s median / 2.6 s p95 (source: Neon docs/blog on cold starts) — again, compute wake, not full environment readiness.

We measured the full gap on our own stack, end to end, and the shape is humbling. Methodology: Kubernetes (EKS), CloudNativePG-managed Postgres, ZFS-backed CSI volumes, pgbench scale=100 (~1.5 GB), checkpoints on, N=5 runs with cold run reported separately, min/median/max — measured on Sandbase’s development stack, 2026-07-02.

Phase min / med / max (s)
S1 — ZFS snapshot + clone ready (the actual CoW) 1.13 / 2.23 / 2.35
S3 — pod scheduled 14.8 / 15.7 / 20.0
S4 — pg_isready (Postgres up, WAL replayed) 10.1 / 18.0 / 18.8
Total branch create, end to end 35.8 / 42.4 / 46.0

Read that table cynically: the copy-on-write clone — the thing every vendor headline is about — is ~3 seconds, about 5% of the total. Pod scheduling plus Postgres bootstrap eat ~33 of the ~42 median seconds. On a local-PV backend the same operation medians 17.8 s; the storage share stays ~3 s in both. Orchestration is ~93% of branch-create time. Anyone who tells you their storage engine is why branching is fast is describing the 5%.

Why does an idle branch cost almost nothing?

An idle database branch should cost approximately nothing, because it is approximately nothing.

Since a fresh clone shares every block with its parent, its incremental footprint is only (a) clone metadata and (b) blocks it has since rewritten. Measured on the same stack: a freshly created branch that inherits ~1.5 GB of data occupies 0.76 MB of its own storage; after 10 minutes idle it stays under 1 MB, and 5 minutes of read-only query traffic adds 0 bytes — reads never trigger a copy, only writes do.

Compression stacks multiplicatively on top, and how you expose the volume matters: on our measurements, lz4 achieved 7.3–7.6× on ZFS datasets versus 3.2–3.3× on zvols for the same pgbench data — the filesystem-aware layout compresses over 2× better than the block-device emulation. Add compute scale-to-zero (suspend the Postgres pod when the branch is idle) and the marginal cost of branch #50 is a few megabytes of changed blocks and zero compute. That’s the entire economic basis for “a branch per PR, per worktree, per agent”: it only works because idle branches round to free.

How do you close the 40-second gap? Warm pools.

A warm pool closes the branch-create gap by moving orchestration off the request path: keep pre-scheduled, pre-bootstrapped Postgres pods idling, and at branch time pay only the seconds-scale copy-on-write clone plus an attach. If ~33 s of a 42 s branch-create is pod scheduling and Postgres bootstrap, the fix is obvious: don’t do those at request time. When a branch is requested, do only the ~3 s CoW clone and attach a warm pod to it; the pool refills in the background. This is standard capacity-pooling — the same reason serverless platforms keep warm workers — and it’s the pattern behind every “instant” branch product whose user-facing latency beats its own orchestration floor. The clone was never the bottleneck; the pool removes everything that was. We’re building exactly this, targeting seconds-not-tens-of-seconds end to end.

What are the honest failure modes?

CoW branching has real trade-offs; anyone selling it without these is selling.

  • Snapshot chains and lineage. A clone depends on its parent snapshot. You can’t casually destroy a parent that has live children; deep chains (agents branching from branches, sometimes hundreds deep) need explicit lineage management and garbage collection.
  • Write amplification. Every first write to a shared block triggers an allocate-and-copy. Write-heavy branches progressively pay back the copy you skipped — CoW is a loan, not a gift. Fine for dev/test/agent workloads (mostly reads plus modest writes); wrong for a write-saturated clone of production traffic.
  • Recovery tax. Cloning a running database’s volume yields a crash-consistent copy: Postgres replays WAL on first start. Skipping a pre-clone checkpoint cost us ~3.7 s of redo at 1.5 GB in testing — it scales with dirty data.
  • Warm pools trade money for latency. Idle pooled pods cost real compute. Pool sizing is a bet on your branch-creation rate.
  • Density has walls. Thousands of volumes per node stress CSI drivers, kernel limits, and control-plane metadata long before they stress the disk. Published density claims in this space are mostly unmeasured; treat them as marketing until someone shows a curve.

Where is this going? Agents don’t ask permission.

Databricks’ State of AI Agents report (Feb 2026) found that agent-created database branches went from 0.1% to 97% of new branches between Oct 2023 and Oct 2025, with roughly 10 branches per project and half of all branch compute living under 10 seconds. The consumer of database branching is increasingly a program in a loop, not a person at a terminal. Programs in loops create branches at machine frequency, hold them for seconds, and abandon them without cleanup — a workload profile that only makes sense on primitives where creation is O(1) and idleness rounds to zero. That’s why this 30-year-old filesystem trick is suddenly the substrate of a product category.


That’s the whole mechanism: shared blocks, pointer-flip clones, and warm orchestration to hide the parts that were never storage. For our part: Sandbase gives every git branch, worktree, PR, and AI agent an instant, isolated, masked copy of your real Postgres database — create, delete, reset, undo, masked by default, with a managed control plane and the data plane in your cloud so your prod data never leaves your account. We’re building it on exactly the primitives above, and we’ll republish these measurements annually with the scripts to reproduce them in ~20 minutes.

Sources

  • Postgres.ai Database Lab documentation — thin clones of 10 TiB in under 2 s.
  • ClickHouse documentation — ALTER TABLE ... FREEZE hard-link snapshots.
  • TigerData, “Fluid Storage” blog — ~500–600 ms block-layer fork claim.
  • Neon documentation/blog — compute cold-start latency (~1.8 s median / 2.6 s p95).
  • PostgreSQL 18 STRATEGY=FILE_COPY reflink benchmark — 6 GB in 212 ms vs 67 s.
  • Databricks, State of AI Agents (Feb 2026) — 97% of new branches agent-created.
  • Sandbase branch-create benchmark scripts + LIMITATIONS.md.
#Postgres#Copy-on-write#Engineering