Understand why distributed systems need consensus, how Raft elects leaders and replicates logs, and how leases and fencing tokens prevent split-brain damage.
What you'll learn: By the end of this lesson you will be able to explain what consensus guarantees and why it needs a majority, walk through Raft's leader election and log replication, describe where Paxos, ZooKeeper and etcd fit, and use leases and fencing tokens to stop a stale leader from corrupting data.
Imagine five friends planning a dinner by group chat. They need to agree on one restaurant. Messages arrive late, some friends drop off the chat, and two people might propose different places at the same moment. The group has to end up with exactly one decision that everyone who answers will accept, and the decision must not flip later, even if the chat is flaky.
That is consensus. Computers face the same problem constantly: which node is the leader, what is the current cluster configuration, who holds this lock, what is the next entry in the log. If two machines each believe they are in charge, they can write conflicting data. Consensus protocols are how a group of unreliable machines safely agrees on a single answer.
The core idea
Consensus lets a group of nodes agree on a value, so that once the decision is made it is never undone, even if some nodes crash and messages are lost or delayed.
A correct consensus protocol provides these properties:
Agreement: no two nodes decide different values.
Validity: the decided value was proposed by some node.
Termination (liveness): if enough nodes are working and the network behaves long enough, a decision is eventually reached.
There is a famous limit: in a fully asynchronous network where even one node may crash, no deterministic protocol can guarantee that it always terminates (the FLP result). Practical protocols respect it by always staying safe (never deciding wrongly) but sometimes becoming temporarily unavailable (no progress) when the network misbehaves. They use timeouts, which are a practical way to make progress most of the time.
The mechanism that makes this work is the majority quorum. A decision needs agreement from more than half of the nodes. Any two majorities overlap in at least one node, so a later decision always meets a node that remembers the earlier one.
Cluster size
Majority needed
Failures tolerated
3
2
1
5
3
2
7
4
3
Even sizes give no extra tolerance (four nodes still need three), which is why clusters have odd sizes. More nodes tolerate more failures but make every decision wait for a larger majority, so most deployments use three or five.
Why you need it
Without consensus, the alternatives are fragile.
A single leader chosen by hand or by a script. If it fails, someone must notice and promote another, and a mistaken promotion creates two leaders.
Leader chosen by "the node with the lowest ID that I can reach". During a network partition, each side thinks the other is dead and elects its own leader. This is split-brain: two leaders accepting conflicting writes.
Timestamps to decide who wins. Clocks disagree, so "latest wins" can discard valid data.
Things consensus is typically used for:
Leader election for a database, scheduler or service.
Replicated state machines: every node applies the same sequence of commands in the same order, so all copies end up identical.
Distributed locks and configuration: a small, strongly consistent store for coordination data.
Cluster membership: deciding which nodes are in the group.
NOTE
Consensus is slow and limited in capacity compared with ordinary databases, because every write needs a majority round trip and is ordered through one leader. Use it for small, critical coordination data, not as your bulk data store. Large systems typically run many consensus groups, each covering a slice of the data.
Raft: consensus designed to be understood
Raft was created to be easier to understand than earlier algorithms while giving the same guarantees. It splits the problem into leader election, log replication and safety. Each node is always in one of three roles.
stateDiagram-v2
[*] --> Follower
Follower --> Candidate: election timeout
Candidate --> Leader: wins majority of votes
Candidate --> Candidate: split vote, new term
Candidate --> Follower: sees current leader or higher term
Leader --> Follower: sees higher term
Terms
Time in Raft is divided into terms, numbered 1, 2, 3 and so on. A term begins with an election and has at most one leader. Terms act as a logical clock: every message carries the sender's term. If a node sees a higher term, it updates its own and steps down to follower. If it receives a message from a lower term, it rejects it. This is how stale leaders are discovered and demoted.
Leader election
All nodes start as followers, expecting periodic heartbeats from a leader.
If a follower hears nothing for a randomised election timeout, it assumes the leader is gone. It increments its term, becomes a candidate, votes for itself, and asks the others for votes.
Each node grants at most one vote per term, on a first-come basis, and only to a candidate whose log is at least as up to date as its own. This is the election restriction, and it guarantees a new leader already holds every committed entry.
A candidate that gets votes from a majority becomes leader and immediately sends heartbeats to assert authority.
If votes split and nobody wins, the timeouts expire again, a new term begins, and a new election runs. Randomised timeouts make repeated ties unlikely.
sequenceDiagram
participant A as Node A
participant B as Node B
participant C as Node C
Note over A,C: Leader has failed, A times out first
A->>A: Term 5, vote for self
A->>B: RequestVote term 5
A->>C: RequestVote term 5
B->>A: Vote granted
C->>A: Vote granted
Note over A: Majority reached, A is leader
A->>B: Heartbeat term 5
A->>C: Heartbeat term 5
Log replication
The leader accepts client commands and appends each as an entry in its log, tagged with the current term and an index.
The leader sends the entry to followers.
Each follower appends it, after checking that its log matches the leader's at the previous entry (the log matching check). If it does not match, the follower rejects it and the leader backs up and resends earlier entries until the logs agree. This repairs followers that missed entries.
When the entry is stored on a majority of nodes, it is committed. The leader applies it to its state machine and replies to the client.
Followers learn the commit point from later messages and apply the entry too.
sequenceDiagram
participant Cl as Client
participant L as Leader
participant F1 as Follower 1
participant F2 as Follower 2
Cl->>L: Command
L->>L: Append to log
L->>F1: AppendEntries
L->>F2: AppendEntries
F1->>L: Success
Note over L: Majority stored, entry committed
L->>Cl: Result
F2->>L: Success
Committed entries are durable and will survive any leader change, because a node with a missing committed entry cannot win an election. Entries that were not yet committed when a leader fails may be lost or may survive, and the client must handle the uncertain outcome, typically by retrying with an idempotency key (see Distributed Transactions).
What a partition does
In a five-node cluster split three and two, the side with three nodes can still elect a leader and commit entries. The side with two cannot reach a majority, so it makes no progress. An old leader stranded on the minority side keeps believing it is the leader until it hears a higher term, but it cannot commit anything, because it cannot get a majority acknowledgement. Raft therefore chooses consistency over availability in a partition, which is the CAP trade-off in action. See CAP and Consistency Models.
Raft property
Guarantee
Cost
One leader per term
No two leaders commit in the same term
Brief unavailability during elections
Majority commit
Committed entries survive minority failures
Write latency includes a quorum round trip
Election restriction
New leader has all committed entries
Lagging nodes cannot become leader
Single leader for writes
Total order of operations
Leader is a throughput bottleneck
TIP
The Raft authors maintain an interactive visualisation and the original paper at raft.github.io. Stepping through an election and a partition there makes this section click.
Paxos, stated accurately
Paxos is the older, foundational consensus family, introduced by Leslie Lamport. Basic Paxos agrees on a single value using proposers, acceptors and learners, with numbered proposals and two phases: a prepare phase to claim a proposal number and discover any value already accepted, and an accept phase to get that value accepted by a majority. Multi-Paxos repeats this to agree on a sequence of values, typically with a stable leader that skips the first phase in the normal case, so it ends up resembling Raft's steady state.
Paxos and Raft give comparable safety guarantees for crash failures. Raft was designed for understandability and a complete specification, while Paxos is subtle to implement and has many variants. Both handle crash faults with a majority; neither handles malicious (Byzantine) nodes, which need different protocols and larger quorums.
ZooKeeper and etcd: consensus as a service
Most teams should not implement consensus themselves. They use a small, well-tested cluster that exposes it through a simple API.
Kubernetes cluster state, service configuration, leader election
Apache ZooKeeper
ZAB (ZooKeeper Atomic Broadcast, a leader-based protocol)
Hierarchical namespace, ephemeral nodes, watches
Coordination for data systems, configuration, locks, group membership
Consul
Raft for its servers
Key-value store, service discovery, sessions
Discovery, health checks, configuration
These stores give you primitives rather than raw consensus: watches that notify you of changes, ephemeral keys that vanish when a client's session ends, and compare-and-set operations. A typical leader election recipe: every candidate tries to create the same key or node; the one that succeeds is the leader; the others watch it, and when the leader's session expires the key disappears and a new election happens.
Keep the data small and the write rate low: these are coordination stores, not general databases.
Leases: leadership with an expiry
Heartbeats and elections handle crashes, but how does a leader know it has been replaced? A lease is a time-limited grant of authority: "you are leader until time T unless you renew". If the leader cannot renew, because it is partitioned or paused, it must stop acting as leader when the lease runs out. Meanwhile, the rest of the cluster waits at least that long before granting a new lease.
Leases let a leader serve reads without a quorum round trip and make locks clean themselves up after crashes. But they rely on bounded clock drift: the holder and the granter must measure roughly the same duration. A holder that pauses for longer than the lease and then wakes up without noticing is the root of the next problem.
Split-brain and fencing tokens
Consider this scenario with a distributed lock that has a lease.
Client A acquires the lock and starts writing to a storage service.
A suffers a long pause (a garbage collection pause, a stalled VM, a slow network). Its lease expires.
Client B acquires the lock and writes to the storage service.
A wakes up, still believing it holds the lock, and writes stale data over B's.
A checks its own clock and finds the lease valid or does not check at all, because the pause happened between the check and the write. No amount of care inside A can fully close that gap. The fix has to be in the resource being protected.
A fencing token is a number that increases with every lock grant. The lock service gives A token 33 and later B token 34. Every write to the storage service includes the token, and the storage service remembers the highest token it has seen and rejects any request with a lower one.
sequenceDiagram
participant A as Client A
participant L as Lock service
participant B as Client B
participant S as Storage
A->>L: Acquire lock
L->>A: Granted, token 33
Note over A: Long pause, lease expires
B->>L: Acquire lock
L->>B: Granted, token 34
B->>S: Write with token 34
S->>B: Accepted
A->>S: Write with token 33
S->>A: Rejected, stale token
Raft's terms play a similar role inside the protocol: a deposed leader's messages carry an old term and are rejected. When you build on top of etcd or ZooKeeper, you can use a revision or transaction id as the token. The storage side must support checking it, so this works best when you control the resource or it offers conditional writes (compare-and-set on a version).
WARNING
A distributed lock without fencing is only an efficiency optimisation, for example to avoid doing duplicate work. It is not safe for correctness-critical mutual exclusion. If a wrong double-execution corrupts data, you need fencing tokens or an idempotent, conditional write at the resource.
Choosing and operating consensus
A few practical guidelines:
Use a managed or proven system. Writing your own consensus is a multi-year project with subtle failure modes. Prefer etcd, ZooKeeper, a managed service, or a database that embeds Raft or Paxos.
Place nodes across failure domains. Three nodes in three availability zones survive the loss of a zone. Three nodes in one rack do not survive the loss of the rack. Cross-region groups survive more but pay in commit latency (every commit waits for a quorum round trip across regions).
Tune timeouts to your network. Too short and transient latency triggers needless elections; too long and failover takes longer than users tolerate.
Monitor leader changes. Frequent elections indicate network trouble, overloaded nodes or poor timeout settings, and each election pauses writes.
Test with partitions and pauses. Fault-injection tests that cut links and freeze processes find the bugs in your use of the lock or lease.
flowchart TD
Q1{"Must exactly one instance act at a time?"} -->|no| NO["Run all instances, make work idempotent"]
Q1 -->|yes| Q2{"Is a duplicate run harmful or only wasteful?"}
Q2 -->|wasteful| LK["Lease or lock without fencing"]
Q2 -->|harmful| Q3{"Can the resource check a token or version?"}
Q3 -->|yes| FN["Lease plus fencing token"]
Q3 -->|no| RD["Redesign: conditional writes at the resource"]
Read it as a ladder of rising cost. Most background work belongs at the top, where a duplicate is harmless because the work is idempotent.
In practice: a job scheduler that needs exactly one active leader on Kubernetes
NOTE
A realistic scenario, not a case history. This is a composite example built from common production patterns. It does not describe any specific company, and every number in it is an illustrative assumption.
The situation
A B2B billing platform runs a scheduler service that triggers around 300 jobs an hour (illustrative): invoice generation, payment retries, usage roll-ups and customer emails. Ten engineers deploy it on Kubernetes with PostgreSQL as the system of record and RabbitMQ carrying work to workers. To survive pod restarts the scheduler runs as three replicas. Right now every replica evaluates every schedule, so some jobs fire twice, and an email goes out twice about once a week (assumption). An earlier attempt to fix it, "only replica 0 schedules", means that when that pod restarts, nothing is scheduled for several minutes. The business wants no duplicate invoices and a failover gap under roughly 30 seconds.
Starting architecture
All three pods can read and fire any schedule, and nothing arbitrates between them.
Options considered
Option
What it fixes
What it costs
Risk
A. Smaller change: one replica, restart on failure
Duplicates in normal operation
Nothing to build
A gap of a minute or more on failure; a node partition can leave a stale pod and a new one running together
A2. All replicas claim due jobs with row locks in PostgreSQL (for example FOR UPDATE SKIP LOCKED)
Duplicates and failover gap, with no election
Rework every job's claim logic; in-memory side effects stay unguarded
Medium: only safe where every job's work is driven from database rows
B. Kubernetes Lease based election, plus an epoch check in PostgreSQL
Duplicates and failover gap, including stale leaders
A client library and one small table
Needs timeout tuning and fault-injection tests
C. Run a separate etcd or ZooKeeper cluster for coordination
Full control of the coordination layer
A new stateful cluster to operate
Heavy for one service; the team would own quorum health
The decision
Field
Entry
Context
Exactly one scheduler must act, a duplicate invoice is costly, the team cannot afford a new stateful cluster, and some scheduler work (email batching, schedule expansion) is not driven by database rows, which rules out A2.
Decision
Adopt B. Pods compete for a Lease object through the Kubernetes API, which stores it in etcd. On winning, the leader increments an epoch in a PostgreSQL row and stamps it on every job it fires. The jobs table only accepts a state change when the stamped epoch is at least the current one. Illustrative timings: 15 s lease, 10 s renew deadline, 2 s retry.
Consequences
Good: automatic failover in tens of seconds, and a paused old leader is harmless. Bad: leadership depends on the API server being reachable, and a CPU-starved leader can flap.
Revisit when
You need more than one active scheduler (partitioned leadership), or failover must be under about 5 seconds.
Do not write your own data into the cluster's etcd. Use the Lease API, which is designed for exactly this.
Rollout and verification
flowchart LR
SH["Shadow mode pods log would-fire"] --> EL["Enable election one leader fires"]
EL --> FZ["Enable epoch checks"]
FZ --> CH["Fault injection kill, partition, pause"]
CH --> PR["Production"]
Start with election in shadow mode: every pod logs "would fire" so you can compare the logs against the old duplicates. Turn on leadership, then epoch enforcement. Before production, run three fault tests: delete the leader pod, block its route to the API server, and freeze it with a stop signal. Dashboards and alerts: leader_transitions_total, lease renewal latency, missed_schedule_seconds (target under the failover budget), fenced_write_rejected_total (any increase is worth a look but proves fencing works), and a count of duplicate fires, which must stay at zero.
The 2 a.m. failure
The page reads: "Scheduler leader changed 5 times in 10 minutes". A node under CPU pressure starves the leader, which fails to renew in time, and the old leader later resumes and writes.
sequenceDiagram
participant A as Old leader
participant API as Kubernetes API
participant B as New leader
participant DB as PostgreSQL
A->>API: Renew lease
Note over A: CPU starved, process stalls
B->>API: Acquire expired lease
B->>DB: Increment epoch to 8
B->>DB: Mark job running with epoch 8
A->>DB: Mark job running with epoch 7
DB->>A: Rejected, stale epoch
Detection: the transitions counter plus a non-zero fenced_write_rejected_total. First three checks: kubectl get lease on the scheduler lease to see the holder, transition count and renew time; kubectl describe the leader pod and its node for resource requests and pressure conditions, plus the pod's CPU throttling metrics; and the logs for "failed to renew lease" messages with API latency. Mitigation: raise the pod's CPU request so renewals are not starved, and temporarily lengthen the lease duration. Follow-up fix: guaranteed resources for the scheduler, an alert on transition rate, and a fault-injection test that freezes the process every release.
Architect's notes
Ask first whether you need a leader at all. If jobs are idempotent, running on all replicas with a database claim is simpler than election.
Measure the failover budget the business really needs. Sub-5-second failover usually costs more than 30 seconds does (tighter timeouts, more risk of flapping).
Fence at the resource. The lease keeps duplicates rare; the epoch check keeps them harmless.
Own the coordination layer. If the cluster team owns the API server, agree what load and latency you can expect from it.
Test pauses, not just crashes. Crash tests pass easily; stalls expose the real bugs.
Defer on purpose. Multi-region leadership adds cross-region quorum latency; do it only with a real requirement.
Common mistakes
Using an even-sized cluster or only two nodes. Two nodes cannot form a safe majority after one fails, and four tolerate no more failures than three. Run three or five.
Using wall-clock time to pick a leader or decide a lock is free. Clocks drift and jump. Use a consensus-backed store and logical terms.
Locks without fencing. A paused client resumes and overwrites newer data. Add fencing tokens or conditional writes at the resource.
Treating etcd or ZooKeeper as a general database. Large values and heavy write rates degrade the whole cluster. Store small coordination data only.
Spreading a quorum over a high-latency link without noticing. Every write then pays the cross-region round trip. Measure commit latency before choosing placement.
Ignoring the minority side during a partition. Clients connected to the minority will see errors or stale reads. Define what they should do, and test it.
Assuming a committed client write is certain after a timeout. A timed-out request may or may not have committed. Retry with idempotency keys.
Key takeaways
Consensus gives durable agreement. Nodes agree on a value that never flips, tolerating crashes and lost messages.
Majorities overlap. That overlap is the reason any later decision sees an earlier one; clusters use odd sizes of 3 or 5.
Raft separates concerns. Randomised-timeout leader election, majority-committed log replication, and an election restriction for safety.
Terms are logical clocks. Messages from lower terms are rejected, so stale leaders are demoted quickly.
Partitions favour consistency. The majority side makes progress; the minority side stops, which is the CAP trade-off in practice.
Paxos and Raft are siblings. Both handle crash faults with a majority; neither tolerates malicious nodes.
Use etcd or ZooKeeper-style services. They expose consensus as watches, ephemeral keys and compare-and-set, and are for small coordination data.
Fence your locks. Leases handle crashes but not pauses; monotonically increasing tokens checked by the resource make stale leaders harmless.
Practice exercises
Exercise 1 — Starter (30 min): For clusters of 3, 5 and 7 nodes, calculate the majority size and how many failures each tolerates. Then explain in two sentences why a 4-node cluster is a poor choice compared with a 3-node or 5-node one.
Exercise 2 — Intermediate (2 hours): Draw a timeline for a five-node Raft cluster where the leader (term 3) is partitioned away together with one follower. Show which side can elect a leader, what term they reach, what happens to client writes sent to the old leader, and what the old leader's log looks like when the partition heals. Then use the interactive visualisation at the Raft website to check your answer.
Exercise 3 — Advanced (half day): Design a job scheduler where exactly one instance should run each scheduled job at a time, using etcd or ZooKeeper for leader election. Specify the election recipe, lease duration and renewal interval, what the scheduler does if it loses its session mid-job, and how fencing tokens or conditional writes protect the job-state store from a paused former leader. Write three fault-injection tests (partition, process pause, coordination-store restart) and the expected behaviour for each.
Related reading
Raft consensus website - the paper, an interactive visualisation, and a list of implementations.
Lesson 4 of 14 · Advanced System Design and Case Studies
Understand why distributed systems need consensus, how Raft elects leaders and replicates logs, and how leases and fencing tokens prevent split-brain damage.
In the rest of this lesson
Paxos, stated accurately
ZooKeeper and etcd: consensus as a service
Leases: leadership with an expiry
Split-brain and fencing tokens
Choosing and operating consensus
In practice: a job scheduler that needs exactly one active leader on Kubernetes
Enrolling is free. It unlocks every lesson in Advanced System Design and Case Studies, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.
Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.
Get new articles by email
Free. We email a confirmation link first, and you can unsubscribe from any email.