Part 7 · Failure
Consensus, Coordination, and Verification
Lesson 16 showed that linearizability — making a distributed register behave as if there were a single copy — is exactly what you need for a uniqueness constraint, a lock, or a leader-election flag, and that it is expensive because it demands coordination. But it left the central question open: how do several unreliable machines actually agree, and agree irrevocably, when any of them can crash mid-decision and the network can lose or delay any message? That mechanism is consensus. This lesson is its home. We will show that consensus, total order broadcast, and a linearizable log are three faces of the same object; why a simple two-phase commit is not fault-tolerant consensus; why a majority is the magic number; what the FLP result says you can never have; and why the practical advice is to need consensus as rarely, and at as small a scope, as you can. Then, because these protocols are too subtle to trust by reading the code, we close with how you actually verify a distributed system — formal specification (TLA+), deterministic simulation testing, and fault injection. This lesson closes Part 7 · Failure and hands to Part 8 · Derived history.
New capability: Recognize when a problem reduces to consensus, size a consensus cluster (how many nodes to tolerate how many failures), explain why 2PC blocks while Raft-style consensus does not, and decide what tiny slice of your system should sit behind ZooKeeper / etcd while everything else stays coordination-free.
1 · The problem: agree on one value, and never take it back
One sentence: consensus is getting several unreliable nodes to irrevocably agree on one value — when any of them may crash and any message may be lost or arbitrarily delayed. "Agree" is a strong word here. A correct consensus protocol must satisfy four properties:
The first three are safety properties ("nothing bad happens"); the fourth is liveness ("something good eventually happens"). Keep that split in mind — the whole drama of consensus is that safety is achievable always, liveness only with extra assumptions.
Why care? Because a surprising number of distributed jobs are consensus wearing a costume. Each of these reduces to "agree on one value, irrevocably":
- Leader election. Among n nodes, agree on exactly one leader — so two nodes don't both think they're in charge (split brain). The decided value is "who is leader for this term."
- Distributed locks / leases. Agree on who holds the lock right now. Lesson 15's fencing token is just the monotonically increasing number consensus stamps on each grant.
- Uniqueness / atomic commit. Agree that a username is claimed, or that a multi-node transaction commits everywhere or aborts everywhere — never half.
- Membership & configuration. Agree on the current set of live nodes, or which shard a key belongs to, so everyone routes the same way.
Lesson 16 ended exactly here: a uniqueness constraint needs a single decider because the two registrations are concurrent and causality alone can't break the tie. Consensus is the machine that breaks the tie and writes the answer down so hard that no later confusion can erase it.
2 · Atomic commit and two-phase commit — and why 2PC blocks
Start with the most familiar consensus-flavoured problem: atomic commit. A transaction touches several nodes (say a payment row on node A and an inventory row on node B). Atomicity (lesson 13) demands all-or-nothing across machines: either every participant commits, or every participant aborts. No participant may commit while another aborts, or you've sold an item you never charged for.
The classic protocol is two-phase commit (2PC). One node acts as the coordinator; the others are participants. It runs in two rounds:
The subtle part is the promise, and it rests on two pieces of durable bookkeeping. When a participant votes "yes" it first writes its vote and all the transaction's changes to stable storage (so a crash-and-restart, lesson 15's crash-recovery model, can still honor the promise) and only then replies. It is now in the in-doubt state — also called prepared: it has surrendered the right to decide for itself, cannot abort, cannot commit, and must do whatever the coordinator says in phase 2. Symmetrically, the coordinator owns a commit log on its own stable storage: the instant it has collected every "yes," it writes a single commit record, and that durable write is the moment of decision — the transaction is now committed even before any participant has heard. (If the coordinator crashes after writing it, recovery reads the log and resends the COMMIT; if it crashes before, recovery aborts.) That one durable record is the single point of agreement that makes the commit atomic. And it is also the flaw.
When 2PC spans different systems — say a relational database and a message broker, each from a different vendor — the standard that lets them play the participant role is XA (the X/Open XA interface), with an external transaction manager as coordinator. XA is exactly the heterogeneous, distributed-transaction form of the protocol above, and it inherits the same blocking flaw plus operational pain (orphaned in-doubt transactions holding locks when the transaction manager is down), which is why most modern designs avoid distributed transactions across services and reach for the outbox / idempotency pattern of lesson 14 instead.
So 2PC (and its cross-system form, XA) is genuinely useful for atomic commit across well-behaved nodes, but it is not the fault-tolerant agreement we're after. The fix is to make the "decision" survive the loss of any single node — which is what total order broadcast and majority-based consensus deliver.
3 · Total order broadcast: the same messages, in the same order, everywhere
Total order broadcast (also called atomic broadcast) is a messaging guarantee with two parts:
- Reliable delivery. No messages are lost: if a message is delivered to one non-crashed node, it is delivered to all of them.
- Totally ordered delivery. Messages are delivered to every node in the same order. If node 1 sees [m1, m2, m3], node 2 also sees exactly [m1, m2, m3] — never [m2, m1, m3].
Picture it as an append-only log that everyone agrees on, position by position:
Here is the equivalence that ties the whole lesson together: total order broadcast is equivalent to consensus — each is implementable from the other. The bridge is the sequence numbers of lesson 16: total-order broadcast is precisely the act of attaching an agreed, gap-free sequence number to every message so all nodes deliver in that order.
The cheap single-leader sequence numbers from lesson 16 are the easiest way to build the broadcast: the leader's append position is the agreed sequence number — but only while one leader holds the pen. Consensus is exactly what lets a new leader pick up the numbering, gap-free, after the old one fails. So deciding the content of each log slot is one consensus instance (slot 3 is "m4" and can never become anything else), and running consensus once per slot rebuilds the broadcast. They are the same problem.
4 · Fault-tolerant consensus: leader, log, majority
The production algorithms — Paxos (and its practical Multi-Paxos), Raft (designed to be understandable), Zab (ZooKeeper's protocol), and Viewstamped Replication — differ in detail but share one shape. Learn the shape once:
Everything rests on one word: majority. A majority quorum is more than half the nodes. Its magic is the same overlap argument you proved for leaderless reads in lesson 09: any two majorities of the same set must share at least one member. That shared node carries knowledge from the old leader's term into the new one — so a freshly elected leader is guaranteed to have seen any entry the previous majority committed. Two disjoint groups can never each commit a conflicting decision, because they can't both be a majority.
n = 3 (f = 1). Majority = 2. If 1 node dies, 2 remain — still a majority, the cluster keeps committing. If 2 die, only 1 remains, which is not a majority of 3, so it correctly refuses to act (better to stall than to split-brain). So 3 nodes tolerate 1 failure.
n = 5 (f = 2). Majority = 3. Lose 2 nodes, 3 remain = a majority, still live. Lose 3 and the remaining 2 are a minority and stop. So 5 nodes tolerate 2 failures.
Why odd numbers? Going from 3 to 4 nodes does not buy more fault tolerance: a majority of 4 is 3, so 4 nodes still tolerate only 1 failure (lose 2 of 4 and you have 2, not a majority) — you added a node, a vote to wait for, and zero extra resilience. 5 is the next useful size. Hence consensus clusters are almost always 3 or 5. The cost of a larger cluster is real: every commit waits for a majority to ack, so 5 nodes means waiting for the 3rd-fastest on every write — more durable, slightly slower, never worth an even count.
Note the availability price you already met in lesson 09: if the cluster cannot reach a majority — say a network partition isolates the leader with a minority — it stops accepting writes rather than risk two decisions. Consensus chooses consistency (safety) over availability under partition. That is the deliberate trade.
5 · The FLP impossibility: you can't always have both safety and liveness
If majority consensus is so clean, why isn't the problem just "solved"? Because of a foundational result, named after Fischer, Lynch, and Paterson — the FLP impossibility:
The intuition: in a fully asynchronous network you cannot distinguish a crashed node from a merely slow one — there is no timeout you're allowed to trust, because messages can legitimately take arbitrarily long. So the algorithm can always be forced into a state where it must either wait forever for a possibly-dead node (losing liveness) or proceed and risk that the node was alive and about to disagree (losing safety). You can't escape the dilemma with cleverness; it's a property of the model.
The escape in practice is to weaken the model. Real networks are not adversarially asynchronous — messages usually arrive within some rough bound. So real consensus systems:
- keep safety unconditionally — they will never decide two different values, no matter how badly the network behaves (the majority-overlap argument needs no timing assumption);
- use timeouts to recover liveness — if the leader is silent for a timeout, followers suspect it has died and elect a new one. The timeout might be wrong (the old leader was just slow), but the term-number fencing makes that harmless: the stale leader's later messages are ignored. Raft adds randomized election timeouts so two followers rarely call an election at the same instant and split the vote.
So FLP doesn't say consensus is impossible — it says you cannot have a timeout-free algorithm that is both always-safe and always-live. The engineering answer is: never sacrifice safety, and buy liveness with timeouts and randomization that work because real networks are usually well-behaved.
6 · Coordination services: ZooKeeper, etcd, and the discipline of needing them rarely
You almost never implement Paxos or Raft yourself. You rent it. ZooKeeper (running Zab) and etcd (running Raft) are off-the-shelf, battle-tested consensus systems — think of them as a small, highly-available, linearizable key-value store with a few extra primitives. Out of the box they give you the consensus-shaped jobs from §1:
7 · Verifying a distributed system: why you test it differently
Everything above is too subtle to trust by reading the code. The bugs that matter — a split brain, a lost commit, a deposed leader's write slipping through — appear only in rare interleavings of crashes, message reorderings, and pauses (§1, lesson 15) that a normal test suite never happens to hit. Ordinary unit and integration tests exercise the happy path and a few hand-picked failures; the dangerous schedules hide in the combinatorial space between them. So distributed systems are verified with three techniques aimed squarely at that space.
The progression is design → implementation → deployment: TLA+ proves the protocol is sound, deterministic simulation proves the code implements it under adversarial schedules, and fault injection proves the deployed system survives real-world faults. None subsumes the others — a verified design can be miscoded, and correct code can be misconfigured. The point that unifies them is the §1 insight: because partial failure is nondeterministic and rare interleavings are where correctness dies, you cannot verify a distributed system by example. You must search the failure space deliberately, with a model checker, a seeded simulator, or an adversary breaking your cluster on purpose.
Trade-offs
| Choice | Buys | Costs |
|---|---|---|
| 2PC (atomic commit) | All participants commit or abort together across nodes | Blocks indefinitely if the lone coordinator dies — not fault-tolerant |
| Majority consensus (Raft/Paxos) | Irrevocable agreement that survives any minority failure; no split brain | Quorum round trip per decision; halts writes when no majority is reachable |
| Total order broadcast | An agreed, replayable log = linearizable state machine | Same as consensus — it is consensus per slot |
| Coordination service (ZooKeeper/etcd) | Rent leader election, locks, config, membership; don't build Paxos | Operational dependency; latency floor; a partition can stall the control plane |
| Elect-one-leader, then no per-op consensus | Linearizable work at local speed under a lease | Must handle lease expiry / fencing correctly or risk a deposed-leader write |
Failure modes
- Split brain. Two nodes both believe they're leader after a partition and both accept writes. Root cause: leadership not backed by a majority, or no fencing token to reject the stale one. Symptom: conflicting committed state, lost updates.
- 2PC coordinator crash. Participants stuck in the uncertain state hold locks forever. Symptom: a transaction hangs, unrelated work blocks on its locks, only a coordinator restart unsticks it.
- Even-sized cluster. 4 or 6 nodes give the same fault tolerance as 3 or 5 while costing an extra vote per commit — and a 2/2 partition deadlocks with no majority either side.
- Consensus on the data plane. Running a quorum write per user operation: every request pays a majority round trip and the whole service goes unavailable during any partition. Symptom: latency cliff and partition-correlated outages.
- Liveness lost to bad timeouts. Election timeout too short → constant leader churn (no progress); too long → slow failover. FLP says you can't make this perfect; tune and randomize it.
Decision checklist
- Does this problem actually reduce to consensus (one irrevocable decision: leader, lock, uniqueness, membership)? If not, keep it coordination-free.
- If you need atomicity across nodes and fault tolerance, use a consensus-backed log, not bare 2PC.
- Size the cluster odd: 3 to tolerate 1 failure, 5 to tolerate 2. Don't pick even counts.
- Are you renting consensus (ZooKeeper/etcd) instead of hand-rolling Paxos? Almost always you should be.
- Is leadership backed by a majority and protected by a fencing token (lesson 15) so a deposed leader's late writes are rejected?
- Is consensus confined to the control plane — elect one leader, then serve linearizable work under a lease with no per-op quorum?
- What happens during a partition? Confirm the minority side correctly stops rather than serving conflicting writes.
Checkpoint exercise
Where this points next
Part 7 · Failure — and with it the whole arc of keeping a single, mutable, consistent picture of state across machines — closes here. We covered replication (08–09), partitioning (11), transactions and isolation (13), the transaction and message boundary (14), the troubles of partial failure and clocks (15), and the consistency / consensus apparatus that buys you single-copy semantics when an invariant truly demands it (16–17) — and how to verify all of it. The recurring lesson is that strong coordination is expensive and should be applied surgically. Part 8 · Derived history turns that insight into an architecture: instead of coordinating every write, treat one dataset as the source of truth and derive everything else — indexes, aggregates, features, embeddings — by reprocessing it. Lesson 18, batch processing, starts there: bounded input in, deterministically recomputed output out, the all-to-all shuffle as the dominant cost, and the property that makes derived data safe — a broken view can simply be deleted and rebuilt from history, no consensus required. We trade coordination on the write path for recomputation on the read path.
Interview prompts
- What exactly does consensus guarantee, and which property is the hard one? (§1 — uniform agreement, integrity/irrevocability, validity are safety; termination is liveness, and §5 shows liveness is the one you can't get for free in an asynchronous network.)
- Why is two-phase commit not fault-tolerant consensus, and what does 3PC change? (§2 — after voting "yes," participants are in-doubt/prepared (durably) and must obey the coordinator, whose single commit-log write is the moment of decision; if the coordinator crashes before that decision reaches them they block indefinitely holding locks, because the decision lives in one node with no majority. 3PC adds a pre-commit round to be non-blocking in theory but assumes a synchronous network and still fails under a partition, so it isn't used.)
- How do you build total-order broadcast from consensus and consensus from total-order broadcast? (§3 — TOB from consensus: run one consensus instance per log slot, deciding the i-th message. Consensus from TOB: every proposer broadcasts its value, TOB delivers all proposals in the same order everywhere, and each node decides the first one delivered — same order means same decision. The agreed sequence number (lesson 16) is the bridge.)
- Explain the equivalence of total order broadcast and consensus, and how linearizability fits. (§3 — deciding each log slot's content is one consensus instance; running consensus per slot builds total order broadcast; a linearizable register/CAS is built by appending to and replaying that agreed log, so all three are one object — the linearizable log of lesson 16.)
- Why do consensus clusters use 3 or 5 nodes and never 4? (§4 — to tolerate f failures you need 2f + 1 nodes with a majority of f + 1; 3 tolerates 1, 5 tolerates 2, but 4 still tolerates only 1 (majority of 4 is 3) while adding a vote to wait for — and a 2/2 partition deadlocks.)
- Why does a majority quorum make leader failover safe? (§4 — any two majorities of the same set share a member (the overlap argument from lesson 09), so a newly elected leader's log necessarily contains every entry a previous majority committed, and two disjoint groups can never both commit conflicting decisions.)
- What does FLP say, and how do real systems get around it? (§5 — no deterministic algorithm guarantees both safety and liveness in a fully asynchronous network with one possible crash; systems keep safety unconditionally and recover liveness with timeouts to suspect dead leaders plus randomized election timeouts, relying on real networks being roughly synchronous.)
- How should you use ZooKeeper/etcd in a system that needs high throughput? (§6 — use consensus to elect a single leader and hold a lease, then let that leader serve linearizable work locally with no per-operation quorum; reserve consensus for the rare control-plane events (leadership, membership, config) and keep the data plane coordination-free.)