Raft from scratch, part 1: leader election & beating split-brain
For two weeks I’ve had a single server. This week I gave it siblings — and hit the oldest problem in
distributed systems: how do a bunch of equal machines, with no boss, agree on a boss? And do it
while messages drop and nodes vanish. This is consensus, and I built the election half of Raft from
scratch to feel exactly why it’s hard.
What I built
A cluster of peers that, from a cold start, elects exactly one leader — and re-elects a new one when
the leader dies, all over the same unreliable network from last week. No coordinator, no shared clock.
term 4: A(follower) B(follower) C(follower)
└─ B times out first ─┐
term 5: B(candidate) ──RequestVote──► A, C
A,C vote yes ──► B has a majority ──► B(leader)
B ──heartbeat every 100ms──► A, C (resets their election timers)
The concepts that made it work
- Terms are a logical clock, and a higher term always wins. Time is sliced into numbered terms,
each with at most one leader. Any peer that sees a term greater than its own instantly steps down and
adopts it. That single reflex is the whole reason two leaders can’t coexist. - Majorities can’t overlap into two leaders. Winning needs a majority, and any two majorities of
the same cluster share at least one member — who only votes once per term. So ≤1 leader per term,
guaranteed by counting, not coordination. - Randomized timeouts are what make it terminate. If every follower timed out at once they’d
split the vote forever. A little jitter (I used 300–600ms) means someone almost always goes first and
wins cleanly. Safety comes from majorities; liveness comes from randomness. - Heartbeats are just elections suppressed. The leader sends empty messages on a timer; each one
resets a follower’s clock. Silence is the only signal a follower needs to challenge for the throne.
The thing that bit me
Two traps, both about concurrency, not consensus. First: it is astonishingly easy to hold a mutex
while making an RPC — and the instant two peers request each other’s vote, they deadlock, each waiting
for a lock the other holds. The fix is a discipline you must never break: lock, read/modify state,
unlock, then call. Second, subtler: when a vote finally comes back, you have to re-ask “am I even
still a candidate in this term?” — because while that message was in flight you may have already
stepped down. Count that stale vote and you conjure a phantom leader. Neither bug fails cleanly; they
show up as flakiness, which is why I only trusted it after it survived repeated runs under the race
detector.
Tradeoffs & the road not taken
What I built is deliberately the election only:
- No replicated log yet. Right now any peer can win, because there’s no data that could be
inconsistent. Real Raft adds a rule — don’t vote for a candidate whose log is behind yours — that
guarantees the new leader has every committed entry. That’s next week’s problem. - No persistence. Term and vote live in memory; a crash-restart could vote twice in one term. Also
next week.
Where this sits in the system
Last week: one server, made honest with retries and dedup. This week: many servers that agree on who’s
in charge, and survive that server dying. Next: replicate the log — the leader accepts commands and
copies them to a majority before calling them committed. That’s when this stops being an election and
becomes a fault-tolerant database.
Part of a series building a distributed database from scratch.