Distributed SQL, from first principles

How CockroachDB survives anything

CockroachDB is a distributed SQL database that looks like a single Postgres node and behaves like a fault-tolerant cluster. Underneath the SQL is a global, sorted key–value map, split into self-healing ranges, each replicated by Raft, ordered by a hybrid logical clock, and versioned with MVCC so that time only ever moves forward. This page takes those pieces apart β€” with simulations you can poke.

Ranges replicated Γ—3 by default Consensus via Raft Clocks: HLC + 500 ms uncertainty Isolation: SERIALIZABLE Storage: Pebble LSM
The premise

One logical database, many machines

A cockroach is famously hard to kill. The database borrows the name because its central promise is survivability: lose a disk, a node, a rack, an availability zone β€” even an entire region β€” and the database stays available and consistent. You keep talking to it in ordinary SQL over the PostgreSQL wire protocol; it quietly handles replication, failover, and rebalancing.

It pulls this off by never trusting a single machine with anything. Every piece of data lives on at least three nodes, and a write is only acknowledged once a majority of those nodes agree on it. That single idea β€” majority agreement β€” is what lets a node die mid-write without losing data or serving a stale read.

🧱

SQL layer

Parses SQL, plans and distributes execution (DistSQL), maps rows to keys.

πŸ”

Transactional layer

Serializable, lock-light transactions using write intents & an HLC.

πŸ—ΊοΈ

Distribution layer

The monolithic keyspace, ranges, and the routing that finds them.

πŸ›‘οΈ

Replication layer

Raft consensus per range; leaseholders serve reads and coordinate writes.

πŸ’½

Storage layer

Pebble, a Go LSM-tree engine, stores versioned key–value pairs on each node.

🌍

Multi-region

Locality-aware placement pins data to regions for latency and survival goals.

Read this page top to bottom and the layers assemble themselves: rows become keys, keys group into ranges, ranges are replicated by Raft, replicas order events with a clock, and the clock makes transactions serializable.
Distribution layer

Everything is one big sorted map

CockroachDB stores your entire cluster — every table, index, and system record — as a single, monolithic, sorted map of key→value pairs. There are no per-table files. The SQL layer's job is to encode rows into keys in that map, choosing the encoding so that rows you tend to read together end up next to each other in key order.

A row's primary-key value becomes part of its key. Take a table users(id PRIMARY KEY, email, name). The row for id = 42 is stored roughly like this:

KeyValue
/Table/users/primary/42/email"ada@example.com"
/Table/users/primary/42/name"Ada"
/Table/users/primary/43/email"grace@example.com"

Because keys are sorted, a range scan like WHERE id BETWEEN 42 AND 99 is a contiguous read. A secondary index is just more keys, encoded so the index column sorts first. This is why the physical layout of a CockroachDB table is really a question about key order.

Why it matters: the sorted keyspace is the substrate for everything below. Splitting the database across machines becomes "cut the sorted keyspace into contiguous chunks" β€” which is exactly what a range is.
Distribution layer

Ranges: the unit of everything

The sorted keyspace is chopped into contiguous chunks called ranges. A range holds a band of keys β€” say [/users/1, /users/9000) β€” and is the atom of replication, distribution, and load-balancing. By default a range aims to stay under 512 MiB; when it grows past that it splits in two, and when adjacent ranges shrink they can merge. Ranges also split under load, so a hot key band can spread across more nodes.

▍ Live keyspace β€” watch ranges split as data grows
0 rows
1 ranges
0 splits
0 MiB avg range size
Each coloured segment is one range. When a range crosses the 512 MiB target it splits at its midpoint β€” no coordination with the rest of the cluster required.

A tiny, always-present meta range acts as an index of ranges: given a key, it tells a node which range holds it and where that range's replicas live. Nodes cache this, so routing a query to the right machine is usually a local lookup.

Replication layer

Raft: how three replicas agree

Every range is replicated β€” three copies by default (five for critical system ranges). Those replicas form a Raft group. Raft is a consensus protocol whose whole purpose is to keep a replicated log identical across machines, even as they crash and recover. At any moment one replica is the leader; it accepts new log entries and replicates them to the followers.

A write becomes durable when a majority (a quorum) has appended it to their log. With three replicas the quorum is two, so one node can fail with zero data loss and no downtime for that range. The surviving majority keeps committing; when the dead node returns, Raft streams it the entries it missed.

▍ Raft simulator β€” elect, replicate, survive a failure
Leader Follower (in sync) Candidate (election) Down Log entry in flight
A five-node group. Propose writes to watch entries replicate and commit once a majority acks. Kill the leader and watch a new election.
Leader β‰  leaseholder. Raft elects a leader for log replication. Separately, one replica holds the range lease and is the leaseholder β€” the single replica allowed to serve reads and to sequence writes. CockroachDB works hard to keep the leaseholder and the Raft leader on the same node so the two roles don't fight.
Replication layer

Leaseholders make reads cheap

If every read had to run a full round of Raft, reads would be as expensive as writes. They don't. For each range, one replica holds a time-bounded lease. That leaseholder has the authoritative, up-to-date copy, so it can answer reads locally β€” no quorum, no network round trip to the followers β€” as long as it reads at or below its own clock and respects outstanding writes.

Writes still go through Raft (a majority must persist them), but the leaseholder coordinates them and applies them in order. Leases are kept alive cheaply using node liveness: a single heartbeat range records that a node is alive, and one heartbeat covers all of that node's leases at once. If a leaseholder dies, its leases expire and another replica picks them up β€” the same failover you saw in the Raft simulator, one layer up.

Read path

Client β†’ route to leaseholder β†’ serve from its local MVCC data at the read timestamp. No consensus needed.

Write path

Client β†’ leaseholder sequences it β†’ Raft replicates β†’ majority acks β†’ applied to Pebble on each replica.

The transactional layer's heartbeat

Hybrid logical clocks: making time monotonic

Here's the hard part of any distributed database: ordering events across machines whose clocks disagree. Physical wall clocks drift. If node A's clock is 30 ms ahead of node B's, then an event that "happened after" another could carry an earlier timestamp β€” and your transactions would see time run backwards.

CockroachDB's answer is the Hybrid Logical Clock (HLC). Each node's clock is a pair:

// an HLC timestamp is two numbers
timestamp = ( physical: wall-clock millis , logical: a counter )

The physical part tracks real time so timestamps stay close to the wall clock (useful for TTLs and AS OF SYSTEM TIME). The logical counter breaks ties and preserves causality: it only ever goes up. The rules are simple and they guarantee timestamps never move backwards:

▍ HLC in motion β€” two nodes, drifting clocks, still causal
Node B's wall clock is deliberately running ~40 ms behind A. Send a message from A to B and watch B's HLC jump forward to preserve causality β€” even though B's physical clock is slower.

Why not Google Spanner's TrueTime?

Spanner uses TrueTime β€” GPS and atomic clocks in every datacentre β€” so it can bound clock error to a few milliseconds and simply wait out the uncertainty on commit. CockroachDB is designed to run on commodity hardware with ordinary NTP, so it can't assume tight clock bounds. Instead of special hardware it uses the HLC plus an explicit uncertainty interval, described next.

The transactional layer

Clock uncertainty & the read that restarts

Because clocks aren't perfectly synced, CockroachDB assumes any two nodes are within a maximum clock offset of each other β€” 500 ms by default (--max-offset). This has two consequences, one for safety and one for liveness.

πŸ›‘ The safety mechanism

If a node measures its clock drifting more than 80% of the max offset (β‰ˆ400 ms) relative to a majority of peers, it spontaneously shuts itself down. A badly-skewed node is more dangerous than a missing one, so CockroachDB removes it rather than let it violate consistency.

πŸ” The uncertainty interval

A transaction reading at timestamp T treats the window [T, T + max_offset] as uncertain. If it encounters a value written inside that window, it can't be sure whether that value is "in its past" or "in its future," so it does the safe thing.

That safe thing is an uncertainty restart: the transaction bumps its read timestamp up past the surprising value and retries the read, now certain it will see it. Crucially, the uncertainty window shrinks with each restart and never grows, so this can't loop forever. The payoff is a strong guarantee: no stale reads for causally-related transactions, without atomic clocks.

Intuition: Spanner spends time waiting on every commit to be safe. CockroachDB spends time occasionally retrying a read to be safe. Same guarantee, different budget β€” one pays on writes, the other pays rarely on reads.
Storage layer

MVCC: writes never overwrite, timestamps only rise

CockroachDB never overwrites a value in place. Every write stores a new version of the key, stamped with the transaction's commit timestamp. The key in Pebble is really (key, timestamp), and versions are sorted newest-first. This is Multi-Version Concurrency Control (MVCC), and it's why the earlier claim β€” "time only moves forward" β€” is literally true in the storage engine.

A read at timestamp T walks down to the newest version with version_ts ≀ T. That gives three superpowers for free:

▍ MVCC timeline β€” versions accumulate, a read finds its version
Click the timeline to move the read timestamp. The highlighted version is the one a read at that instant returns β€” always the newest version at or before the pointer.

Old versions don't pile up forever. A background MVCC garbage collector reclaims versions older than the zone's gc.ttlseconds β€” 25 hours by default β€” as long as no protected timestamp (e.g. a running backup or changefeed) still needs them. That 25-hour window is also the horizon for how far back AS OF SYSTEM TIME can travel.

The transactional layer, assembled

Distributed transactions, serializable by default

Now the pieces combine. CockroachDB runs transactions at SERIALIZABLE isolation by default β€” the strongest level, with no anomalies β€” across keys that may live on completely different machines. (A weaker READ COMMITTED level is also available when you want fewer retries and can tolerate its anomalies.) It does this without a central lock manager.

The life of a write transaction

  1. Pick a timestamp. The gateway node stamps the transaction with its HLC β€” this becomes the provisional commit timestamp.
  2. Lay down write intents. Each write is stored as a provisional MVCC version β€” a write intent β€” that points at a single transaction record. An intent is a value and a lock in one.
  3. Resolve conflicts. A reader or writer that meets an intent looks up the transaction record: if that txn committed, the intent is promoted to a real value; if it's still pending, the newcomer may wait or force a restart.
  4. Commit. Flip the transaction record to COMMITTED. Parallel Commits lets the intents and the record be written in one round of consensus instead of two, roughly halving commit latency.
  5. Clean up. Asynchronously rewrite intents into normal MVCC versions. Readers that arrive first will do it for you.

Serializability is enforced with a timestamp cache (remembering the highest timestamp at which each key was read, so a later write can't invalidate that read) and the uncertainty logic from earlier. When a write would break serial order, the transaction's commit timestamp is pushed forward; if that push can't be proven safe, it restarts at the higher timestamp. Every timestamp you saw rising in the HLC and MVCC demos is what makes this ordering possible.

The through-line: ranges give you distribution, Raft gives you durability, leaseholders give you cheap reads, the HLC gives you a global order, uncertainty intervals make that order safe on cheap clocks, and MVCC records the order permanently. Serializable transactions are what you get when all five cooperate.
Operating the cluster

Gossip, rebalancing & multi-region

πŸ“‘

Gossip

Nodes share cluster metadata β€” liveness, store capacity, network addresses β€” over a peer-to-peer gossip protocol, so no node is a single point of truth.

βš–οΈ

Rebalancing

An allocator continuously moves and adds replicas to even out size and load, and to repair under-replicated ranges after a failure.

🌐

Multi-region

Locality flags let you place replicas by region and set survival goals β€” survive a zone, or survive a whole region β€” trading latency against resilience.

Zone configurations are the knobs for all of this: num_replicas, constraints (place replicas in region=eu-west), lease_preferences (keep the leaseholder near your users for fast reads), and gc.ttlseconds. You can set them per cluster, database, table, index, or even row range.

From simulation to the real thing

Talk to a real cluster

Everything above runs entirely in your browser. This last panel talks to an actual CockroachDB cluster through the Worker's /api endpoints β€” reading its live hybrid logical clock, listing the real nodes in its gossip network, and showing how its keyspace is split into ranges. It's read-only, and it's dormant until a cluster is attached.

▍ Live cluster
checking…
Attach your own free cluster. Spin up a free CockroachDB Cloud Serverless cluster, create a read-only SQL user, and connect it to this Worker in one of two ways (see the project README): a Hyperdrive binding, or a DATABASE_URL secret via wrangler secret put. The moment it's connected, this panel lights up β€” no redeploy of the teaching content needed.