Choosing one node to be in charge, and reliably choosing another when it dies. Simple to describe, and the place where split brain lives.
Prerequisites: Failure Modes, Coordination Services Time to read: ~20 minutes
Lots of things need exactly one node in charge:
You need to elect one, and — the hard part — elect a new one when the current leader dies, without ever having two at once.
🚨 The difficulty follows directly from failure detection being impossible: you cannot distinguish “the leader is dead” from “the leader is slow” from “I can’t reach the leader.” Every leader election protocol is a way of making a safe decision despite that ambiguity.
“The node with the lowest IP is the leader.” Fine until it dies, and now nobody agrees on who’s next — because different nodes have different views of who’s alive.
“A human decides.” Works, but the recovery time is however long it takes to page someone. Your MTTR is now measured in tens of minutes.
“Everyone votes.” Better — this is the right shape. But two nodes on opposite sides of a network partition can each get votes from their side and both declare victory. Hence quorums.
🚨 The thing that makes this hard.
5 nodes. Node 1 is leader. A network partition splits them 2 | 3.
Side A: nodes 1, 2 — node 1 still believes it is leader, keeps accepting writes
Side B: nodes 3, 4, 5 — can't reach node 1, elects node 3 as leader, accepts writes
Two leaders. Both accepting writes. The data diverges.
When the partition heals, which writes are real?
And it’s worse than it looks, because the old leader doesn’t know it’s been deposed. From its perspective, some peers stopped responding — a normal-looking condition.
Split brain has caused real, serious incidents, including GitHub’s 2018 outage where a 43-second partition caused divergence that took 24 hours to reconcile.
The primary defence. A leader requires votes from a strict majority.
5 nodes, split 2 | 3:
Side A (2 nodes): cannot reach 3 votes → NO leader → refuses writes
Side B (3 nodes): reaches 3 votes → elects a leader → serves
📐 Because there can be only one majority in any partition, there can be only one leader. This is why quorum-based systems are safe, and why they’re CP — the minority side deliberately becomes unavailable rather than risk divergence.
🚨 This is also why cluster sizes are odd. 4 nodes need 3 for a majority — the same failure tolerance as 3 nodes, with more coordination overhead. Use 3 or 5.
Quorum stops a new leader emerging on the minority side. It doesn’t stop the old leader — which was partitioned, or paused by GC — from resuming and writing.
1. Node 1 is leader with epoch 5. It's partitioned.
2. Nodes 3,4,5 elect node 3 as leader with epoch 6.
3. Node 1's network recovers. It still thinks it's leader (epoch 5).
4. Node 1 writes to storage... which REJECTS it, because it has already seen epoch 6.
Every leadership term gets a monotonically increasing number (epoch, term, fence token, generation). Every write carries it. The resource being protected rejects anything with a stale number.
🚨 The critical detail: the protected resource must enforce the check. If storage happily accepts whatever it’s given, the token is decorative. This is the same problem as distributed locking, and it’s the most commonly missed part of both.
Leadership isn’t permanent; it’s a lease with a TTL that must be renewed.
Leader holds a lease for 10 seconds; renews every 3 seconds.
If it can't renew (partitioned, or dead), the lease expires and it MUST stop acting as leader.
Others wait for the full lease duration before electing a replacement.
✅ Self-healing: a dead leader’s lease expires without anyone intervening. 🚨 But leases depend on clocks, and clocks disagree (Time & Clocks). A leader whose clock runs slow may believe its lease is still valid after others have declared it expired. Leases reduce the split-brain window; they don’t eliminate it. You still need fencing.
“Shoot The Other Node In The Head” — forcibly power off or isolate the old leader before promoting a new one, typically via out-of-band management (IPMI) or by revoking its storage access.
Blunt, effective, and used in traditional HA clusters. Requires infrastructure control you often don’t have in the cloud.
Don’t implement consensus yourself. Use etcd, ZooKeeper, or Consul.
The ephemeral sequential node pattern:
/election/
├── node-0000000001 ← client A (lowest = LEADER)
├── node-0000000002 ← client B (watches node-1)
└── node-0000000003 ← client C (watches node-2)
🚨 Watching the predecessor rather than the leader is deliberate — it avoids the herd effect where all N-1 nodes wake simultaneously on a leadership change and stampede the coordination service. It’s a nice detail to mention.
With etcd it’s simpler, since it has leases and a campaign API built in:
session, _ := concurrency.NewSession(client, concurrency.WithTTL(10))
election := concurrency.NewElection(session, "/my-service/leader")
election.Campaign(ctx, myID) // blocks until we become leader
// ... do leader work, checking session.Done() ...
On Kubernetes, use the built-in Lease object — the same mechanism Kubernetes’ own controllers use. 🎙️ “If we’re on Kubernetes I’d use a Lease rather than deploying ZooKeeper — leader election is already solved there.” That’s the answer most candidates miss.
Worth knowing by name:
Bully algorithm. The highest-ID reachable node wins. A node noticing the leader is gone challenges all higher IDs; if none respond, it declares itself leader. Simple, chatty, and prone to split brain because it has no quorum requirement.
Ring algorithm. Election messages circulate a logical ring, accumulating IDs; the highest wins. Fewer messages, but fragile to node failures during the election.
Raft’s election. The one used in practice: randomized election timeouts prevent nodes from campaigning simultaneously; a candidate needs a majority; terms (epochs) provide fencing. → Consensus
🚨 Bully and Ring are textbook algorithms, not production ones. If asked, note that they lack quorum requirements and are therefore unsafe under partition — that’s the insightful answer.
The leader is a bottleneck and a single point of failure. Every design with one should say how long a failover takes and what happens during it.
📐 The failover timeline, honestly:
Leader dies
→ 5–15 s failure detection (lease expiry / missed heartbeats)
→ 1–5 s election
→ 1–10 s new leader warms up (cold caches, connection pools, state recovery)
─────────
~10–30 s of degraded or unavailable service
🚨 That warm-up phase is routinely forgotten and is often the largest component. A newly promoted database primary has an empty buffer pool and serves everything from disk for a while.
Shorten it with: pre-warmed standbys (a replica already caught up and holding a warm cache), and tuned but not-too-aggressive detection timeouts.
Clients must handle it: retry with backoff, re-resolve who the leader is (don’t cache it forever), and tolerate the window. A client that caches leader identity indefinitely keeps hammering a deposed node.
🚨 The best answer is often “don’t have one.” This is a strong thing to propose.
Partition the work instead. If each node owns a disjoint set of keys, no coordination is needed at all:
❌ One leader assigns work to 10 workers
✅ Worker N handles users where hash(user_id) % 10 == N
No election, no split brain, no failover window. Rebalancing on node failure is the only coordination needed, and consistent hashing handles it. → Consistent Hashing
Make operations idempotent so duplicate execution is harmless, removing the need for exactly-one semantics. → Idempotency
Use leaderless replication (Dynamo-style quorums) where any node accepts writes. → Quorums
Use a queue — many consumers, each message to one of them. The broker handles distribution; you need no leader.
🎙️ “Rather than electing a leader to distribute work, I’d partition by user ID so each worker owns a disjoint slice. That removes the election, the split-brain risk, and the failover window entirely — designing coordination away is more robust than implementing it correctly.”
| Decision | Gain | Cost |
|---|---|---|
| Have a leader | Simple reasoning; a single ordering authority | Bottleneck, SPOF, failover window, split-brain risk |
| Quorum-based election | Provably at most one leader | Minority side unavailable; needs 3+ nodes |
| Fencing tokens | Old leader can’t corrupt data | The protected resource must enforce it |
| Leases | Self-healing; no manual intervention | Clock-dependent; reduces but doesn’t remove the window |
| Short detection timeout | Fast failover | False positives from GC pauses; disruptive churn |
| No leader (partitioned work) | No election, no split brain, no failover | Requires partitionable work; rebalancing complexity |
kube-controller-manager
and kube-scheduler run multiple replicas, only one active. It’s a good model to copy rather than
reinvent.min-replicas-to-write setting that mitigates it) are a good
practical read on the topic.1. Build election with etcd. Three instances of a small program using etcd’s election API. Watch one become leader. Kill it. Time how long until another takes over — that number is your failover MTTR, and measuring it yourself is worth more than reading about it.
2. Cause split brain. Run a 3-node cluster in Docker. Use iptables (or docker network
disconnect) to isolate the leader. Confirm the majority elects a new one. Then reconnect the old
leader and observe what it does — does it step down cleanly, or does it try to act as leader?
3. Demonstrate the need for fencing. Have the leader hold a lease and write to a file. kill
-STOP it for longer than the lease TTL, let another node take over, then kill -CONT it. The
original writes with a stale epoch. Now add epoch checking on the file writer and watch the stale
write get rejected.
4. Measure the warm-up cost. Promote a database replica to primary under load. Graph latency for the first two minutes. The recovery curve is the part people forget.