Engineering Atlas › System Design Fundamentals

Raft consensus step by step

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Roles and terms

Every server is a follower, candidate, or leader. Terms are logical clocks that only increase. Any message carrying a higher term makes the receiver adopt that term and step down to follower. Each server persists currentTerm, votedFor, and its log before answering RPCs.

Leader election

A follower that hears nothing from a leader for an election timeout, randomised in a range such as 150 to 300 ms (Raft paper), increments its term, votes for itself, and sends RequestVote to all peers. A peer grants its vote if it has not voted in this term and the candidate's log is at least as up to date as its own: a higher last-entry term wins, and with equal terms the longer log wins. A candidate with a majority becomes leader and immediately sends heartbeats. Randomised timeouts make split votes rare; when one happens, everyone times out again at different moments.

Log replication

Clients send commands to the leader. It appends the entry locally with the current term and index, then sends AppendEntries(prevIndex, prevTerm, entries, leaderCommit). A follower rejects the call unless its entry at prevIndex has term prevTerm. On rejection the leader moves nextIndex for that follower back and retries; on success the follower deletes any conflicting suffix and appends. This log matching property means that if two logs agree at one index and term, they agree on everything before it.

Commit rule

The leader marks an index committed once a majority stores it and the entry is from the leader's current term. Older-term entries become committed indirectly when a current-term entry after them commits. Without this rule a new leader could count replicas of an old entry, commit it, crash, and have a different leader overwrite it; Figure 8 of the paper shows the exact sequence.

Membership and snapshots

Logs cannot grow forever, so each server snapshots its state machine and discards the prefix. A lagging follower receives InstallSnapshot. Adding or removing servers goes through the log, either one server at a time or with joint consensus, so two disjoint majorities never exist.

Down the stack

Layer 1The contract

Raft's paper lists five properties (its Figure 3) you can test with a fault injector: at most one leader per term, a leader never overwrites its own log, two logs that share an index and term agree on every earlier entry, every committed entry appears in every later leader's log, and no two servers apply different commands at the same index. On top of that, a Raft-backed store such as etcd offers linearizable writes, and linearizable reads only if the read goes through the leader with a quorum check. A follower read without that check is serializable and may be stale. Progress needs a live majority; failover costs roughly one election timeout plus a vote round, and in-flight client requests come back with unknown outcome.

Layer 2The mechanism

etcd's Raft library is the most widely copied design. It no longer lives inside the etcd repository: etcd v3.6 moved it to its own repo and renamed the module go.etcd.io/raft/v3 (etcd v3.6 changelog), so older import paths and blog posts that say go.etcd.io/etcd/raft are out of date. It is a deterministic state machine with no I/O: the application calls Step(msg) and Tick(), then drains a Ready struct containing HardState (term, vote, commit), new Entries to persist, Messages to send, CommittedEntries to apply and an optional Snapshot. The ordering rule is strict: persist HardState and Entries before sending the messages that depend on them, and only then call Advance() (etcd-io/raft). Message types include MsgApp, MsgAppResp, MsgVote, MsgPreVote, MsgHeartbeat and MsgSnap.

Production additions matter more than the base algorithm. PreVote makes a would-be candidate ask whether it could win before bumping its term, so a node returning from a partition cannot depose a healthy leader (section 9.6 of Ongaro's dissertation). etcd turns it on by default (--pre-vote=true in v3.6) (etcd docs). CheckQuorum makes a leader step down if it has not heard from a majority within an election timeout. ReadIndex serves linearizable reads by recording the commit index, confirming leadership with one heartbeat round, and waiting for the state machine to catch up, all without writing to the log. Flow control caps in-flight MsgApps per follower so a slow follower cannot blow up leader memory. The apply side writes into a bbolt B+tree keyed by MVCC revision.

Layer 3The machine

The commit path is network plus fdatasync. etcd's WAL is a series of preallocated 64 MB segment files; each record carries a type, a CRC and the protobuf payload, and preallocation means appends do not need to update file size metadata on every sync. The leader can write to its own disk in parallel with sending MsgApp to followers, so a commit costs roughly max(local flush, fastest-majority round trip plus their flush). etcd's own guidance treats a WAL fsync p99 above about 10 ms as a sign of trouble (etcd FAQ), which is why shared or burstable cloud disks cause leader churn: a stalled flush delays heartbeats, followers time out, and a new election starts.

Batching is what makes throughput reasonable. Many proposals that arrive while a flush is running are written and synced together, and a single MsgApp carries many entries up to a configured byte limit. Snapshots bound recovery time: after a configured number of applied entries the state machine is snapshotted and the WAL prefix released, so restart replays megabytes, not gigabytes. etcd v3.6 cut that default from 100,000 to 10,000 entries (etcd v3.6 changelog); the tuning guide notes a lower count helps when memory or disk use is too high, and settings copied from older guides may be ten times too high. Because every member stores the full dataset in memory-mapped bbolt pages, etcd keeps a backend quota of a few gigabytes; a request is also capped (1.5 MiB by default) so one large value cannot stall replication for everyone (etcd docs).

Why it's built this way

  1. Consensus bugs come from ambiguity, so Raft forces a strong leader and a contiguous log, trading some flexibility for a protocol people can implement correctly.
  2. A single leader means every write crosses one machine's NIC and disk, so batching, pipelining and parallel local fsync are required to reach useful throughput.
  3. Votes and log entries must survive crashes, so the WAL is synced before any reply, and disk latency directly sets commit latency and election stability.
  4. Timeouts are the only failure detector, and they misfire under GC pauses and partitions, so PreVote and CheckQuorum exist to stop needless elections.
  5. Logs grow without bound, so snapshots and compaction exist, and a lagging follower is sent a snapshot instead of the missing entries.

Diagram

stateDiagram-v2
    [*] --> Follower
    Follower --> Candidate: election timeout
    Candidate --> Candidate: split vote, new term
    Candidate --> Leader: votes from majority
    Candidate --> Follower: sees higher term or leader
    Leader --> Follower: sees higher term
    Leader --> Leader: heartbeat, replicate, commit

Worked example

A 3-node etcd cluster in one region: RTT 0.5 ms, WAL fsync 1 ms on good SSDs. A write costs the leader's fsync in parallel with one follower round trip plus that follower's fsync, so commit is about 2 ms p50. With 5 nodes the leader still only needs 2 followers, so latency barely moves, but each write now ships to 4 peers and leader egress roughly doubles.

Failure case: heartbeat 100 ms, election timeout 1,000 ms (etcd defaults; the tuning guide asks for a heartbeat near the member round trip and an election timeout at least ten times it, up to 50 s for global clusters (etcd tuning)). The leader's host freezes. After about 1 s, followers time out, one wins in a single round trip, and writes resume at roughly 1.1 s. A slow disk gives a different picture: fsync p99 of 800 ms delays heartbeats, followers time out, and you see repeated elections even though nothing has crashed. etcd's guidance to put the WAL on dedicated SSD exists for exactly this reason.

Read cost: a deposed leader serving from memory can return stale data. ReadIndex adds one heartbeat round (about 0.5 ms); lease reads skip it but assume bounded clock drift.

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
3 votersLowest write fan-outSurvives only 1 failureSingle-region control planes
5 votersSurvives 2 failures or a zone plus a nodeMore egress, slower electionsCritical metadata across 3 zones
Non-voting learnersExtra read replicas, no quorum costReads may lagRead scale or a remote standby
ReadIndex readsLinearizable without clock assumptionsOne RTT per read batchDefault for correctness
Lease readsLocal reads, no RTTUnsafe if clocks drift past the boundRead-heavy, well-run clock sync

Pitfalls in production

Leadership lens

Check yourself

  1. Why does a voter reject a candidate whose log is shorter but whose last term is equal?

    Show answer

    The candidate may be missing committed entries. Electing it could let it overwrite entries a majority already holds.

  2. Why can a leader not commit an older-term entry just by counting replicas?

    Show answer

    A later leader with a newer last term could still be elected without that entry and overwrite it. Committing a current-term entry first makes the prefix safe.

  3. What does PreVote prevent?

    Show answer

    A rejoining isolated node with an inflated term forcing a healthy leader to step down.

  4. Why does a 5-node cluster not commit faster than a 3-node one?

    Show answer

    Commit needs a majority acknowledgment, so latency is set by the second-fastest follower; adding nodes adds fan-out, not speed.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →