S073 · Consensus — Paxos & Raft Intuition
How a group of machines that can crash, lag, or lie by omission agree on the same value. The algorithm powering etcd, ZooKeeper, Kafka, CockroachDB, and every serious cluster's brain.
🎯 Explain why consensus is hard, walk through Raft's leader election + log replication + safety without notes, and name three real systems whose lifeblood is a Raft cluster.
Why this session exists
Every distributed system eventually needs a group of computers to agree on one fact: "who's the leader?", "what's the next log index?", "which replica is up-to-date enough to serve reads?". That sounds trivial until you add crashes, network partitions, and asynchronous messages — and then you discover it is the hardest problem in the field. Consensus is the algorithm that makes it possible. Once you understand Raft you understand how Kubernetes' etcd stays consistent, how Kafka picks controllers, how CockroachDB commits, how ZooKeeper keeps the world's ZooKeepers happy. Miss this session and every future "how does X coordinate?" answer is hand-waving.
- State the FLP impossibility result in one sentence and explain how Raft dodges it in practice.
- Walk through Raft's three sub-protocols (leader election, log replication, safety) without notes.
- Compare Paxos and Raft on 'ease of understanding', 'academic priority', and 'production adoption'.
- Name at least four production systems whose brain is a Raft cluster and one whose brain is Paxos.
- Diagnose a split-brain incident by asking 'did you have a quorum?'.
Prerequisites
- S072 · Consistency models — consensus is the mechanism that delivers linearizability across nodes.
- S068 · Replication basics — leaders and followers must already be familiar words.
- S042 · Networking · TCP — you need to know that "the message was sent" is not the same as "the message was received".
(a) Intuition · 5 min
Five friends are on a group call trying to pick a restaurant. The phone line is bad — messages randomly drop, some people freeze for 30 seconds and rejoin. You want a rule that guarantees: if the group ever settles on a choice, everyone who eventually rejoins agrees on that same choice. No two subgroups can convince themselves the pick was different.
The trick is: nothing counts as "decided" until a majority has explicitly agreed. Any subgroup with fewer than a majority might have missed messages from the majority — so it must defer.
Raft is that rule, formalised. One friend is designated the "leader" for a term. She proposes each next choice. It only becomes committed once she has heard "yes" from a majority of the group (including herself). If she disappears, whoever else notices first says "term is over, elect a new leader" and holds an election — again decided by majority.
The whole algorithm rests on one property: two majorities of the same group always overlap. So no split-brain can commit two different values in the same term.
The impossibility result you can't dodge — and how Raft dodges it anyway
- In an asynchronous network (no bound on message delay) with even one crashed node, no deterministic consensus algorithm can guarantee termination.
- Translation: you cannot promise 'we WILL agree in bounded time' when the network can be arbitrarily slow.
- Raft's dodge: use randomised election timeouts. In practice the network is 'partially synchronous' — most messages arrive within, say, 50 ms — and randomisation breaks symmetry so elections finish quickly.
- This is the difference between 'impossible in theory' and 'works fine in production every day'. Understand which one applies before you argue on Twitter.
A quick history — why Raft won even though Paxos was first
- 1989Paxos · LamportFirst algorithm to prove consensus is achievable under partial synchrony. Notoriously hard to understand.
- 1998'The Part-Time Parliament' publishedLamport's Paxos paper, written as a parable about a Greek island. Rejected years earlier for being too weird.
- 2006Chubby · GoogleFirst famous industrial use — a Paxos-backed lock service that underpins BigTable, GFS.
- 2010ZooKeeper (ZAB) releasedYahoo ships a Paxos-variant coordination service. Powers HBase, Kafka, Hadoop.
- 2014Raft · Ongaro & OusterhoutStanford paper: 'consensus, but explainable in a lecture'. Community immediately adopts it.
- 2015etcd + CoreOS + Kubernetesetcd (Raft) becomes the brain of Kubernetes. Raft is now the default choice for new systems.
- 2021Kafka KRaft modeKafka replaces its ZooKeeper (ZAB) dependency with its own Raft implementation. Even old Paxos systems migrate.
(b) Visual walkthrough · 15 min
Raft's state machine at a glance
The commit path — how a client write becomes durable
Raft's three sub-protocols
Followers wait for heartbeats. If none arrive within a randomised timeout (150-300 ms), they become candidates, increment term, and request votes. Whoever gets a majority becomes leader.
The leader appends each client command to its log, then sends AppendEntries to all followers. Once a majority (including leader) has stored the entry, it is 'committed' and applied to state machines.
Two guarantees: (a) only a candidate whose log is at-least-as-up-to-date as a majority can win an election, (b) a leader never overwrites its own log entries. Together these prevent split-brain commits.
What lives at which layer
Raft in the stack of a real database
Paxos vs Raft — same job, very different vibes
The academic OG
- Two roles: proposers and acceptors, no explicit leader
- Multi-Paxos adds a leader as an optimisation
- Written in a style even Lamport's colleagues struggled with
- Still the choice in Google's Chubby, Spanner, Megastore
- Better for weird topologies (Flexible Paxos, quorum systems)
Consensus with a manual
- Explicit leader is central, never optional
- Strong leader restriction: only leader appends to log
- Log 'holes' forbidden; strict linear log
- Now dominant: etcd, Consul, CockroachDB, TiKV, MongoDB (config), Kafka KRaft, Nomad, RethinkDB, InfluxDB Enterprise
- ~1000 LoC to implement; Paxos triple that
Paxos-cousin, still widely deployed
- ZooKeeper Atomic Broadcast — bespoke consensus for total-order broadcast
- Powers Kafka (pre-KRaft), HBase, Hadoop namenode failover
- Higher operational overhead than etcd — hence the industry drift to Raft
- Worth knowing for legacy systems, not for new designs
The mental model to hold
"Raft elects a leader, and the leader decides. So consensus is basically just leader election plus following orders."
Leader election is the easy half and it is not even required for safety — Raft remains correct with no leader, it simply makes no progress. The hard part is guaranteeing that a new leader never loses a committed entry, across arbitrary crashes and partitions where several nodes may have believed themselves leader. That guarantee comes from the election restriction and the commit rules, not from having a leader at all.
The myth is sticky because election is the visible, animated, easily-diagrammed part of every Raft explanation, and because "one node decides" is a satisfying resolution to the problem. It misleads because it suggests the difficulty is choosing, when the difficulty is that a leader can be deposed at any moment without knowing it — it may be partitioned away and still believe it leads. Every subtle Raft rule exists to make that situation safe.
The question that separates understanding from familiarity:
A leader in term 5 is partitioned away.
The majority elects a new leader in term 6.
The old leader has not noticed and still accepts writes.
Why is this SAFE?
Because the old leader cannot COMMIT: commit requires
acknowledgement from a majority, and it cannot reach one.
Its writes stay uncommitted and are overwritten when it
rejoins and learns of term 6.Safety comes from what commit requires, not from preventing two nodes believing they lead. Raft allows that situation and makes it harmless.
Why must a quorum be a strict majority? Why not any fixed number of nodes, like two out of five?
- 1A decision is only safe if a later decision cannot contradict it, which requires the later decider to learn about the earlier decision.forced by · two contradictory committed values would break the single-value guarantee consensus exists to provide
- 2The only way to guarantee that learning is to require that any two decision-making groups share at least one member, who carries the knowledge from one to the other.forced by · nodes communicate only with those they can reach; a shared member is the only guaranteed information path between two groups that may never talk directly
- 3Two subsets of a set of N nodes are guaranteed to intersect if and only if their sizes sum to more than N.forced by · if they summed to N or less, they could be disjoint, and a common element would not be guaranteed
- 4If all quorums are the same size Q, the requirement becomes 2Q > N, which gives Q > N/2 — a strict majority.forced by · this is the smallest size for which any two quorums must overlap, regardless of which nodes each contains
- 5Choosing anything smaller, such as two out of five, permits two disjoint quorums, each unaware of the other, each committing a different value.forced by · two disjoint groups of two exist within five nodes, so a partition can produce exactly that split
Therefore the majority requirement is a set-theoretic necessity: it is the minimum condition under which two decisions must share a witness. It is not a conservative convention.
And note what this predicts: cluster sizes should be odd, because five nodes tolerate two failures and six nodes also tolerate only two — the sixth node adds cost and latency while adding nothing to fault tolerance. It also predicts that read and write quorums need only satisfy R + W > N rather than both being majorities, which is exactly the tunable knob Dynamo-style systems expose. And it predicts why witness or arbiter nodes work: they contribute to the intersection without storing data, because all the overlap argument requires is a participant, not a replica.
Consensus is not about agreeing on a value; it is about agreeing on the order of a log. One node is elected to propose entries, and an entry becomes committed once a majority has stored it. Every replica applies committed entries in order, so every replica reaches the same state.
Terms are logical time. Every message carries a term, and any node seeing a higher term immediately steps down — which is how a stale leader discovers it is stale without any central authority telling it.
- Commit means a majority has durably stored the entry. Only committed entries may be applied and returned to clients — an entry present on the leader but not yet on a majority can still be lost, and treating it as done is the bug consensus exists to prevent.
- A candidate can only win an election if its log is at least as up to date as a majority's. This single restriction is what guarantees a new leader already holds every committed entry, so nothing committed is ever lost.
- Consensus gives you an ordered log, not a database. Everything else — state machines, leader leases, membership changes, snapshots — is built on top, which is why Raft appears as a component inside etcd, Kafka's controller, and every distributed database rather than as a product.
- Latency is bounded by the round trip to the slowest member of the majority. This is why consensus groups are kept within a region or a small number of nearby regions, and why a geographically stretched cluster is slow on every single write rather than only during failures.
Fire this model when you see: an etcd or ZooKeeper cluster with an even number of nodes · a cluster spanning distant regions · repeated leader elections under load · a system that lost data during a partition · someone implementing leader election with a database lock.
You need to coordinate distributed state. Adopt a consensus system, or design so consensus is unnecessary?
Never implement consensus yourself. The algorithms are subtle, the failure cases are rare and catastrophic, and correct implementations took years of production hardening plus formal verification to reach their current state. Use etcd, ZooKeeper, or a well-tested library.
More importantly, put consensus on the control path and keep it off the data path. The systems that scale are the ones where a coordinated decision is made rarely and cached — assign a partition once, then serve millions of requests against that assignment with no further coordination. Consensus per request is the design that looks correct on a whiteboard and cannot be made fast afterwards.
(c) Hands-on · 25 min
Let's build a toy Raft — enough to see leader election happen. We won't do full log replication (that's 500+ lines); we'll implement the election protocol so you can watch a partition heal and re-elect.
#!/usr/bin/env python3
"""raft_election.py — a toy of Raft's leader election in one file.
Run: python raft_election.py
No deps. Simulates 5 nodes, a network with adjustable drop rate,
and prints a timeline of terms + votes + wins.
Not production Raft — no log replication, no persistence, no snapshots.
Enough to feel the protocol.
"""
from __future__ import annotations
import random
import threading
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
random.seed(7)
class Role(Enum):
FOLLOWER = "F"
CANDIDATE = "C"
LEADER = "L"
@dataclass
class VoteReq:
term: int
candidate: str
@dataclass
class VoteResp:
term: int
granted: bool
voter: str
@dataclass
class Heartbeat:
term: int
leader: str
class Network:
"""Simulated network with a drop rate + latency range."""
def __init__(self, drop: float = 0.0, latency: tuple[float, float] = (0.005, 0.02)):
self.drop = drop
self.latency = latency
self.nodes: dict[str, "Node"] = {}
self.partitions: set[frozenset[str]] = set()
def register(self, node: "Node") -> None:
self.nodes[node.name] = node
def partition(self, group_a: set[str], group_b: set[str]) -> None:
"""Prevent any message between group_a and group_b."""
for a in group_a:
for b in group_b:
self.partitions.add(frozenset([a, b]))
def heal(self) -> None:
self.partitions.clear()
def send(self, src: str, dst: str, msg: object) -> None:
if frozenset([src, dst]) in self.partitions:
return
if random.random() < self.drop:
return
delay = random.uniform(*self.latency)
def deliver() -> None:
self.nodes[dst].receive(src, msg)
threading.Timer(delay, deliver).start()
class Node:
def __init__(self, name: str, peers: list[str], net: Network):
self.name = name
self.peers = peers
self.net = net
self.role = Role.FOLLOWER
self.term = 0
self.voted_for: Optional[str] = None
self.leader: Optional[str] = None
self.votes_received: set[str] = set()
self.lock = threading.Lock()
self.stopped = False
self._reset_election_timer()
# Start the tick loop
threading.Thread(target=self._tick, daemon=True).start()
def _reset_election_timer(self) -> None:
self.election_deadline = time.time() + random.uniform(0.15, 0.30)
def _tick(self) -> None:
while not self.stopped:
time.sleep(0.01)
with self.lock:
if self.role == Role.LEADER:
self._send_heartbeats()
elif time.time() > self.election_deadline:
self._start_election()
def _start_election(self) -> None:
self.role = Role.CANDIDATE
self.term += 1
self.voted_for = self.name
self.votes_received = {self.name}
self._reset_election_timer()
self._log(f"election started, term={self.term}")
for p in self.peers:
self.net.send(self.name, p, VoteReq(self.term, self.name))
def _send_heartbeats(self) -> None:
for p in self.peers:
self.net.send(self.name, p, Heartbeat(self.term, self.name))
def receive(self, src: str, msg: object) -> None:
with self.lock:
if isinstance(msg, VoteReq):
self._handle_vote_req(msg)
elif isinstance(msg, VoteResp):
self._handle_vote_resp(msg)
elif isinstance(msg, Heartbeat):
self._handle_heartbeat(msg)
def _handle_vote_req(self, m: VoteReq) -> None:
if m.term > self.term:
self.term = m.term
self.voted_for = None
self.role = Role.FOLLOWER
grant = (m.term == self.term
and (self.voted_for is None or self.voted_for == m.candidate))
if grant:
self.voted_for = m.candidate
self._reset_election_timer()
self.net.send(self.name, m.candidate, VoteResp(self.term, grant, self.name))
def _handle_vote_resp(self, m: VoteResp) -> None:
if self.role != Role.CANDIDATE or m.term != self.term:
return
if m.granted:
self.votes_received.add(m.voter)
majority = (len(self.peers) + 1) // 2 + 1
if len(self.votes_received) >= majority:
self.role = Role.LEADER
self.leader = self.name
self._log(f"WON election, term={self.term}, votes={self.votes_received}")
def _handle_heartbeat(self, m: Heartbeat) -> None:
if m.term >= self.term:
self.term = m.term
self.role = Role.FOLLOWER
self.leader = m.leader
self.voted_for = None
self._reset_election_timer()
def _log(self, msg: str) -> None:
print(f"[{time.time()%100:6.2f}] {self.name}: {msg}")
def stop(self) -> None:
self.stopped = True
def main() -> None:
net = Network(drop=0.0)
names = ["n1", "n2", "n3", "n4", "n5"]
nodes = {n: Node(n, [p for p in names if p != n], net) for n in names}
for n in nodes.values():
net.register(n)
print("--- start: watch a leader emerge ---")
time.sleep(1.5)
# Simulate a partition isolating the current leader
leader = next((n for n in nodes.values() if n.role == Role.LEADER), None)
if leader:
others = {n.name for n in nodes.values() if n.name != leader.name}
print(f"\n--- partition {leader.name} away from the rest ---")
net.partition({leader.name}, others)
time.sleep(1.5)
print("\n--- heal the partition ---")
net.heal()
time.sleep(1.0)
for n in nodes.values():
n.stop()
if __name__ == "__main__":
main()What each block does
Anatomy of the script
Modify the partition call:
net.partition({"n1", "n2"}, {"n3", "n4", "n5"})Watch the two-node group elect candidates that never win. Set net.heal() after 2 seconds and observe the higher-term leader from the group of 3 win everyone back. Now try net.partition({"n1","n2","n3"}, {"n4","n5"}) — the group-of-3 wins (has majority) and the group-of-2 fails.
(d) Production reality · 15 min
A misconfigured deploy to Cloudflare's global etcd cluster (used as the control plane for Workers KV) caused a majority of etcd nodes to fail health checks and be removed from the cluster. Suddenly the surviving nodes were fewer than quorum.
Raft did exactly what it's supposed to do: it stopped accepting writes rather than risk inconsistency. But that meant the Workers KV control plane was frozen — no new key writes could propagate globally.
Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
Teach these three, no notes:
- Why is consensus hard? (FLP + the group-chat analogy)
- Walk through a Raft leader election in 60 seconds.
- Why do 3-node Raft clusters tolerate only 1 failure? (majority math)
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.