Consensus, Clocks, and the Ordering Problem27 min read
Module A-1·27 min read
Raft leader election and log replication, quorums and split brain — plus why wall-clock timestamps cannot order events, and what TrueTime buys with atomic hardware.
Module A-1 — Consensus, Clocks, and the Ordering Problem
What this module covers: Why getting several machines to agree on one value is genuinely hard rather than merely fiddly, starting from the impossibility result that shapes every real implementation. Quorums, why majorities prevent split brain, and why a two-node cluster is worse than one node. Raft in enough detail to reason about it — terms, elections, log replication, and the safety property that makes it correct — plus the latency floor consensus imposes and the rule that follows: use it for metadata, not for the data path. Then the half that causes more production damage: time. NTP drift and the monotonic-versus-wall-clock bug, why wall-clock timestamps cannot order cross-region writes, and how last-write-wins turns that into silent data loss. Lamport clocks, vector clocks and hybrid logical clocks as three different answers, what Spanner's TrueTime buys by paying for atomic hardware, and the discipline of never using a wall clock for correctness.
Agreement Is Hard Because You Cannot Tell a Dead Node From a Slow One
Module P-10 established the fact this module is built on: the only failure detector available over a network is a timeout, and a timeout cannot distinguish a crashed process from a paused one, a slow one, or one behind a broken route. Every hard problem in distributed coordination is a consequence.
Now add the thing you actually want. Five machines must agree on one value — who the leader is, what the shard map says, whether the lock is held — such that they never disagree, even if two of them crash and the network partitions.
There is a formal result about this, and it is worth knowing precisely because it explains why every real system looks the way it does. The FLP impossibility result: in an asynchronous system where messages can be arbitrarily delayed, no deterministic algorithm can guarantee consensus if even one process may fail. Not "it is difficult" — it cannot be guaranteed, because there is always an execution in which the algorithm fails to decide.
Real systems escape this in the same way, by weakening the model rather than the goal:
Assume partial synchrony. Messages usually arrive within some bound. Then use timeouts as a failure detector, accepting that a wrong guess costs availability (a spurious election) rather than correctness.
Give up liveness under sustained bad conditions, never safety. A Raft cluster with a flapping network may elect leaders repeatedly and make no progress. That is the designed behaviour: it stops rather than deciding two different values.
So every consensus protocol you will use is a machine for turning "we cannot be sure who is alive" into "a majority has agreed, and no other majority can have agreed to something else."
Analogy: a committee that must appoint one chair, whose members can only exchange letters, and where a member who goes quiet might be dead, asleep, or writing a very long letter. The rule that makes it work is not better letters — it is requiring a majority to sign the appointment. Two different majorities of the same committee must share at least one member, and that member will not sign twice for the same term. The overlap is the mechanism; everything else is bookkeeping.
Quorums: Majorities Overlap, Which Is the Entire Safety Argument
A quorum is ⌊n/2⌋ + 1 — a strict majority. The property that matters is that any two majorities of the same set intersect in at least one member, so a value committed by one majority cannot be contradicted by another.
Nodes
Quorum
Failures tolerated
Notes
1
1
0
no fault tolerance, and no coordination cost
2
2
0
worse than one node — either failure stops all writes
3
2
1
the practical minimum
4
3
1
same tolerance as 3, more cost, slower writes
5
3
2
the standard choice for anything important
7
4
3
rarely worth the latency
Two conclusions people get wrong.
Two nodes are strictly worse than one. With two nodes a quorum is both of them, so any single failure halts writes — you have doubled your failure surface and gained nothing. Every "we'll add a second node for redundancy" plan for a consensus system is wrong; the increment that buys something is the third.
Even numbers waste a node. Four tolerates the same single failure as three while requiring one more acknowledgement per write. Use odd counts.
And the thing majorities buy you: split brain is impossible. Partition five nodes into 3 and 2. The side of 3 can form a quorum and keeps operating; the side of 2 cannot and stops. There is never a moment when both sides commit conflicting values — which is precisely what CAP describes (Module P-10) as choosing consistency over availability on the minority side.
What a majority quorum does not do is prevent a node from believing it is still the leader. The old leader on the minority side may accept reads, answer health checks, and hold a lock it no longer owns. It cannot commit anything, because it cannot reach a quorum, but it can still do damage through side effects — which is the entire subject of Module A-2's fencing tokens.
Raft: Terms, Elections, and a Replicated Log
Raft is the consensus algorithm to reason with, because it was explicitly designed to be understandable and because it is what etcd, Consul, CockroachDB, TiKV and Kafka's KRaft mode implement.
Structure. One leader at a time; followers replicate its log. Time is divided into terms — a monotonically increasing integer, incremented on each election. A term is Raft's logical clock and its fencing mechanism at once: any message carrying a stale term is rejected, which is how a deposed leader is neutralised the moment it talks to anyone current.
Election. Each follower runs an election timeout, randomised (say 150–300 ms). If no heartbeat arrives before it fires, the follower increments the term, becomes a candidate, and requests votes. A node grants at most one vote per term. A candidate winning a majority becomes leader.
The randomisation is doing real work: with identical timeouts, every follower would become a candidate simultaneously, split the vote, and repeat indefinitely. Randomised timeouts make one node reliably fire first. This is Module A-6's jitter argument appearing at the bottom of the stack.
Log replication. Clients send writes to the leader, which appends to its log and sends AppendEntries to followers. Once a majority has persisted an entry, the leader marks it committed and applies it to its state machine. Committed means durable and irrevocable.
The safety property that makes it correct: a candidate can only win an election if its log is at least as up to date as the majority that votes for it. Combined with "a leader never overwrites its own committed entries," this guarantees that a committed entry is present in every future leader's log. That is the whole correctness argument, and it is worth being able to state, because it is what makes "committed" mean something.
Membership changes are the subtle part. Moving from a 3-node to a 5-node cluster naively creates a window where two disjoint majorities exist (2 of 3, and 3 of 5), so two leaders can be elected. Raft solves it with joint consensus — a transitional configuration requiring majorities of both old and new sets. The practical lesson: cluster membership changes are the most dangerous operation on a consensus system, they must go through the protocol rather than a config file, and one node at a time.
What consensus costs, and the rule that follows
Every committed write requires a round trip to a majority. So:
Latency floor = the RTT to the median-slowest quorum member. Same-AZ, that is under a millisecond. Cross-region, it is 50–150 ms per write, and no amount of engineering removes it, because it is the speed of light plus routing.
Throughput is bounded by the leader, which handles every write. Consensus does not scale writes; it makes them safe. Scaling means sharding into many consensus groups (Module P-6), each with its own leader — which is exactly what CockroachDB, TiKV and Spanner do.
Availability is bounded by quorum reachability, so a cluster spanning three regions survives a region loss and pays cross-region latency on every write.
Hence the design rule that matters more than any protocol detail: use consensus for metadata, not for the data path. Who is the leader, what is the shard map, which nodes are members, who holds the lease — these are small, infrequently-changed, and correctness-critical, so paying 5 ms or 100 ms is fine. Routing every user write through a consensus group is how you build a system with a 150 ms floor on every request.
This is why the common architecture is a consensus system beside your data store rather than underneath it: Postgres replication is not consensus, but Patroni uses etcd (which is) to decide who the primary is. The expensive guarantee is applied only to the decision that needs it.
Now the Other Half: Clocks Are Not a Source of Truth
Consensus gives you agreement on an order of operations. The tempting shortcut is to skip it and use timestamps instead — order events by when they happened. That shortcut is responsible for a large fraction of silent data loss in distributed systems, and the reasons are physical.
NTP keeps clocks approximately synchronised, and approximately is doing a lot of work. A well-run NTP setup holds machines within a few milliseconds of each other; a badly-run one drifts by hundreds of milliseconds; a VM resumed from suspension can be off by minutes. Clocks are also adjusted, which means they can step backwards — a machine whose clock was 300 ms fast has it corrected, and now() returns an earlier value than it did a moment ago.
Which produces the single most common clock bug in application code:
// BROKEN: wall clock can step backwards or jump forwards mid-measurement. const t0 = Date.now(); await doWork(); const elapsed = Date.now() - t0; // can be negative, or wildly wrong
// CORRECT: a monotonic clock only ever moves forward, and is unaffected by NTP. const t0 = process.hrtime.bigint(); await doWork(); const elapsedMs = Number(process.hrtime.bigint() - t0) / 1e6;
The rule: wall clocks are for displaying and recording human time; monotonic clocks are for measuring durations. Every timeout, lease, rate-limit window and latency measurement wants the monotonic one. A lease whose expiry is evaluated against a wall clock can be extended or expired by an NTP correction, which is a safety failure rather than an inconvenience.
Wall-clock timestamps cannot order cross-region writes
Here is the failure that costs data:
Region EU (clock +40 ms fast) Region US (clock −30 ms slow) t=10:00:00.100 write A t=10:00:00.150 write B (actually 50 ms LATER) timestamp recorded: 00.140 timestamp recorded: 00.120
Replication converges. Last-write-wins picks the higher timestamp → A wins. The later write, B, is discarded. No error. No conflict reported. The user's edit is simply gone.
The clocks were off by 70 ms combined — a completely ordinary amount — and the recorded order is the reverse of the real order. Now note what "last-write-wins" actually did: it did not resolve a conflict, it chose one write and destroyed the other, silently, using evidence that was wrong.
This is worth stating as a design rule: last-write-wins is not a conflict resolution strategy, it is a data loss strategy with a tolerable failure rate. It is acceptable when losing one of two concurrent writes is genuinely fine (a cache entry, a presence flag, a user's theme preference). It is not acceptable for anything additive — a shopping cart, a set of collaborators, a counter, a ledger — because the lost write was information nobody can reconstruct.
Three Answers: Lamport Clocks, Vector Clocks, Hybrid Logical Clocks
If physical time cannot order events, order them logically.
Lamport clocks. Each node keeps a counter. Increment on every local event; when sending a message, attach the counter; on receipt, set local = max(local, received) + 1.
A: 1 ──► 2 ─────────────► 3 ╲ msg(2) B: 3 ──► 4
This guarantees that if event X caused event Y then L(X) < L(Y). What it cannot do is the converse: L(X) < L(Y) does not mean X happened before Y — they may be concurrent. So a Lamport clock gives you a consistent total order (ties broken by node ID) but cannot detect a conflict, which is exactly what you need to know before choosing to discard a write.
Vector clocks. Each node keeps a counter per node. Comparing two vectors gives three outcomes rather than two:
That third outcome is the whole point: vector clocks detect concurrency, which lets the system surface a genuine conflict instead of guessing. Dynamo and Riak used them to return siblings — both versions — leaving resolution to the application, which is honest but pushes real work onto every caller. The cost is size: a vector grows with the number of writers, so a system with many clients needs pruning heuristics, and pruning can reintroduce false conflicts.
Hybrid logical clocks (HLC) combine both, and are what modern systems actually use. A timestamp is (physical, logical): take the max of your physical clock and any timestamp you have seen; if the physical component did not advance, increment the logical counter.
// Monotonic, causally consistent, and still close to real time. function hlcNow(last: {p: number, l: number}, seen?: {p: number, l: number}) { const wall = Date.now(); const p = Math.max(wall, last.p, seen?.p ?? 0); const l = (p === last.p && p === seen?.p) ? Math.max(last.l, seen.l) + 1 : (p === last.p) ? last.l + 1 : (p === seen?.p) ? seen.l + 1 : 0; return { p, l }; }
HLC gives you three properties at once: timestamps that respect causality, timestamps that are comparable to real time (so a human can read them and a query can filter on them), and constant size. Its bound is that it tracks physical time only as well as your clocks do — a badly skewed clock pushes HLC timestamps into the future and they stay there, because the clock is monotonic by construction. CockroachDB and MongoDB both use HLCs, with a configured maximum tolerated skew beyond which a node removes itself from the cluster rather than corrupting ordering.
Detects causality
Detects concurrency
Comparable to real time
Size
Wall clock
no
no
yes
tiny
Lamport
yes
no
no
tiny
Vector
yes
yes
no
O(writers)
HLC
yes
no (single value)
yes
tiny
TrueTime
yes
n/a — provides real order
yes, with bounds
tiny + hardware
And CRDTs, where convergence has to be automatic
Vector clocks tell you a conflict exists; somebody still has to resolve it. If there is no human in the loop — offline-first clients, collaborative editing, multi-region active-active — the data type itself must be built so that concurrent updates merge deterministically. That is a CRDT: a grow-only counter that merges by taking the max per replica; an add/remove set that keeps tombstones so a remove cannot be undone by a stale add; a sequence type (as in collaborative text editors) where concurrent insertions interleave to the same result on every replica.
The trade is that you give up arbitrary semantics for guaranteed convergence — a CRDT set can converge on "both items added," but it cannot express "only one of these two may exist," because that requires coordination. Module A-9 develops this where it belongs, in multi-region design; the point here is that when last-write-wins is unacceptable and no human can arbitrate, a CRDT is the alternative, not a cleverer clock.
What TrueTime Buys, and What It Costs
Google's Spanner takes the opposite approach: rather than abandoning physical time, make the uncertainty explicit and bounded.
TrueTime is a time service backed by GPS receivers and atomic clocks in every datacentre. Its API does not return an instant, it returns an interval:
TT.now() → [earliest, latest] with (latest − earliest) typically 1–7 ms
The guarantee is that the true absolute time lies inside that interval. And that turns time into something you can reason about: instead of "my clock says X" you have "the real time is definitely between X−ε and X+ε."
Spanner uses it via commit-wait: after a transaction picks a commit timestamp, it waits out the uncertainty window before making its writes visible. That wait guarantees that any transaction starting afterwards, anywhere on the planet, will see a strictly later timestamp — which gives external consistency, the strongest practical guarantee: if transaction A completes before transaction B begins in real time, A's timestamp is smaller, globally, with no coordination between them.
What that buys is remarkable: globally distributed, strongly consistent, linearizable reads and writes with SQL, without vector clocks or application-level conflict resolution.
What it costs, and why nobody else simply copies it:
Specialised hardware in every datacentre, plus the operational discipline to keep ε small. Cloud Spanner rents you this; you cannot build it on commodity VMs.
Commit latency has a floor equal to ε, plus consensus round trips. Every commit pays it.
ε is a correctness dependency. If uncertainty grows — a GPS antenna fails, a clock drifts — Spanner's latency degrades, deliberately, rather than its correctness. That is the right trade and it means clock health is a production SLI.
The transferable lesson for systems that are not Spanner: the useful move is not "trust the clock" or "ignore the clock" but bound the uncertainty and design against the bound. A lease held for 30 seconds with a 10-second safety margin is a system reasoning about clock error explicitly. A lease that assumes clocks agree is one that will hand the same lock to two holders.
Why this matters in production: clock problems do not announce themselves. There is no error when a timestamp is 200 ms wrong; there is a lost edit, a lease held by two owners, a rate-limit window that resets twice, a monitoring graph with a gap or a spike that never happened, or a JWT rejected as not-yet-valid because the issuer's clock is ahead of the verifier's. Every one of those is diagnosed by checking clock offset — and almost nobody checks clock offset, because nothing points there. ntp_offset on every host is one of the cheapest and least-collected metrics in existence (Module O-1).
Where This Shows Up in Your Stack
PostgreSQL: streaming replication is not consensus — there is no automatic, safe leader election in core Postgres, which is why HA tooling (Patroni, Stolon) delegates that decision to etcd or Consul. Even synchronous_commit = remote_apply gives durability on a replica, not agreement about who the primary is; promote two primaries and you have split brain with no protocol to prevent it. For time: always timestamptz, know that now() is transaction-start time while clock_timestamp() is the actual current instant (the distinction that produced Module P-23's polling bug), and never derive ordering across nodes from now().
Node.js:Date.now() for recording when, process.hrtime.bigint() for measuring how long — the wall-clock-elapsed bug is endemic and produces negative durations and impossible latency percentiles. JWT nbf/exp validation is a clock dependency between two services, so a small clockTolerance is not sloppiness, it is acknowledging reality.
Redis: Sentinel and Cluster failover are not consensus-safe. Failover can lose acknowledged writes because replication is asynchronous, and a partitioned master keeps accepting writes until it notices. This is fine for a cache and unacceptable for a lock or a counter that matters — the direct setup for Module A-2's Redlock discussion.
Kafka: the modern control plane (KRaft) is Raft, replacing ZooKeeper for metadata — the canonical "consensus for metadata, log replication for data" split. acks=all with min.insync.replicas=2 on a 3-replica partition is a quorum-shaped durability guarantee, and the leader epoch is exactly Raft's term doing exactly Raft's job: fencing a zombie leader whose messages carry a stale epoch and are rejected.
etcd / ZooKeeper / Consul: consensus you rent rather than write. Use them for leader election, configuration, membership, service registries and leases (Module A-5). Respect their nature: small values, low write rates, and an odd number of nodes — an etcd cluster used as a general-purpose datastore will fail in exactly the way this module's latency section predicts.
Summary
The only failure detector on a network is a timeout, and FLP says no deterministic algorithm can guarantee consensus in a fully asynchronous system with even one possible failure. Real protocols escape by assuming partial synchrony and by giving up liveness under sustained bad conditions while never giving up safety — a flapping cluster stops making progress rather than deciding two values.
A quorum is a strict majority, and the safety argument is that two majorities must overlap. Two nodes are strictly worse than one, because a quorum of two tolerates zero failures; even node counts waste a node; three is the minimum and five is the standard for anything important.
Majorities make split brain impossible for commits, not for beliefs. A deposed leader on the minority side cannot commit, but it can still act — which is why fencing exists (Module A-2).
Raft's term is both its logical clock and its fencing mechanism: any message with a stale term is rejected, neutralising a deposed leader the instant it speaks. Randomised election timeouts prevent perpetual split votes, an entry is committed once a majority has persisted it, and the safety property is that a candidate must have an up-to-date log to win — which is what makes "committed" irrevocable. Membership changes are the most dangerous operation and must go through the protocol, one node at a time.
Consensus does not scale writes; it makes them safe. The latency floor is a round trip to the median-slowest quorum member — sub-millisecond in one AZ, 50–150 ms across regions — and throughput is bounded by the single leader. Therefore use consensus for metadata, not the data path: leadership, shard maps, membership, leases. Scale by sharding into many consensus groups.
Wall clocks are for recording human time; monotonic clocks are for measuring durations. NTP corrections can step a wall clock backwards, so Date.now() deltas produce negative and nonsensical elapsed times, and any lease or timeout evaluated against a wall clock is a safety bug rather than an inaccuracy.
Wall-clock timestamps cannot order cross-region writes. Seventy milliseconds of combined skew — an ordinary amount — inverts the recorded order of writes fifty milliseconds apart. Last-write-wins is therefore not conflict resolution, it is data loss with a tolerable failure rate: fine for a theme preference, wrong for anything additive like a cart, a collaborator set, or a counter, because the discarded write is information nobody can reconstruct.
Lamport clocks respect causality but cannot detect concurrency, so they give a total order without telling you whether a conflict exists. Vector clocks detect concurrency — the property you need before deciding to discard a write — at a size that grows with the number of writers. Hybrid logical clocks give causality, constant size, and timestamps comparable to real time, which is why CockroachDB and MongoDB use them; their bound is that a badly skewed clock pushes timestamps forward permanently, so they pair with a maximum-skew threshold beyond which a node ejects itself.
When no human can arbitrate a conflict, the data type must converge on its own — a CRDT — and the price is that you cannot express constraints requiring coordination, such as uniqueness (Module A-9).
TrueTime makes uncertainty explicit and bounded — an interval rather than an instant — and Spanner's commit-wait pays out that interval to obtain external consistency globally. It costs atomic clocks and GPS in every datacentre and a commit-latency floor equal to the uncertainty, and clock health becomes a correctness dependency. The transferable lesson is the method, not the hardware: bound your clock error and design against the bound, rather than trusting or ignoring the clock.
Clock problems are silent, producing lost edits, double-held leases, JWTs rejected as not-yet-valid, and monitoring graphs with impossible shapes. ntp_offset per host is among the cheapest and least-collected metrics there is.
The next module, Distributed Locks and Coordination, is this module's consequences made concrete. Everything above says a node can believe it holds something it does not — so a lock built on a timeout is a lock that will occasionally have two holders, and the mechanism that saves you is not a better lock but a fencing token carried into the resource being protected. It also asks the more useful question: which of your locks can be designed away entirely.
Knowledge Check
A team runs a 3-node etcd cluster for leader election and, wanting more redundancy before a launch, adds a fourth node by editing the configuration file on each host and restarting them one at a time. What does the module say about both the node count and the method?
A multi-region product stores user documents in two active regions and resolves replication conflicts by keeping the version with the later updated_at timestamp, set by the writing region's clock. Users occasionally report that an edit they made "didn't save," though the UI confirmed it. Clock offsets across regions are typically 30–70 ms. What is happening, and what is the correct framing of the fix?
An engineer proposes running the company's primary transactional workload directly on a 5-node Raft-based key-value store spanning three regions, arguing that consensus gives the strongest possible correctness guarantees. What does the module say?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.