Inside Raft: Write and Read Paths, Leader Election, and Recovery

25 minute read

Published:

1. A Replicated Database

Consider a key-value database running on three nodes: A, B, and C. Each node has its own copy of the data. A client sends SET x = 10; the database must preserve that write even if one node fails and must prevent replicas from applying conflicting writes.

Raft coordinates this through a replicated log. One node acts as leader, orders writes, and sends log entries to followers. Once a write is committed, each node applies it to its own database. We will start with A already elected as leader, follow a write and a read, then examine what happens when A fails.

Raft assumes crash failures: nodes may stop, restart, or lose connectivity, but do not deliberately send false information. It is used in databases, metadata stores, configuration services such as etcd and Consul, and coordination systems.

What Each Node Contains

ComponentRole in this database
Raft logRecords ordered commands, including entries that are not yet committed.
Application state machineApplies committed commands in order. SET x = 10 changes the local value of x.
Local storage engineMaintains the resulting user data, local data structures, caches, and persistent files.

The storage engine is part of each database node. Its data is already the replica’s database state; there is no later transfer to a separate database server.

An illustrative disk layout is:

raft-log.dat       -> log entries
raft-metadata.dat  -> current term and vote
snapshot.dat       -> application state at a recorded log position
state/             -> storage-engine files containing the applied data

state/ could contain database pages, SSTables, manifests, and a storage-engine WAL. An embedded engine such as RocksDB manages these files and in-memory structures; caches need not survive a restart. Implementations may share storage between the Raft log and application state. Query planning, shard placement, and cluster rebalancing belong to other database components.

The state machine must be deterministic: the same starting state and ordered commands must produce the same logical result. Replicas can have different file layouts or cache contents while storing the same values.

System Overview

Raft overview: leader A replicates log entries to B and C; each node applies committed commands to its local storage engine.
One Raft group. The nodes are shown after applying the same command; they do not have to apply it simultaneously.

2. Write Path: Follow One Command

Starting State

A is the leader; B and C are followers. All three have applied entries through index 100 and store x = 5. Entry 100 was created in term 7, which is also the current term.

A term is an election epoch. A node increments its current term when it starts an election. An election may fail, so a term can end without a leader.

An index is a position in the log. Each entry records its index, creation term, and command. Terms identify which leader epoch produced an entry; indexes determine application order.

Example of a log entry:

index: 100
term: 7
command: SET x = 5

We will track four values on every node:

ValueMeaning
lastLogIndexHighest index stored in the local Raft log. An uncommitted entry may later be replaced by a leader.
commitIndexHighest index this node knows is committed and is allowed to apply to its database.
lastAppliedHighest entry this node has applied to its database.
Stored xThe value currently in the local database.

In this example, A commits entry 101 from its current term after confirming durable copies on A and B—a majority of the three nodes. Followers advance their commit index when the leader announces commitment.

lastLogIndex tracks stored commands, commitIndex tracks commands known to be committed, and lastApplied tracks commands already executed. For entry 101 containing SET x = 10, these positions change separately:

Stage on one nodelastLogIndexcommitIndexlastAppliedDatabase value
Entry 101 is stored, but not known committed101100100x = 5
Node learns entry 101 is committed101101100x = 5
Node applies entry 101101101101x = 10

Commitment does not execute the command. The application loop processes committed entries in order:

while lastApplied < commitIndex:
    apply log[lastApplied + 1] to the database
    advance lastApplied

Application may lag while earlier commands are being processed or the storage engine is busy. Thus lastApplied <= commitIndex; a read that must include entry 101 waits until lastApplied reaches at least 101.

For the write walkthrough, all three nodes start at index 100:

NodeLast log indexcommitIndexlastAppliedStored x
A · leader1001001005
B · follower1001001005
C · follower1001001005

The client sends SET x = 10 to A. If it contacts a follower instead, the service may redirect or proxy the request to the leader.

Step 1: A Appends the Entry

A assigns the next index and records:

index: 101
term: 7
command: SET x = 10

In this example A persists the entry before sending it to followers. Only A’s log changes; its database still contains x = 5.

NodeLast log indexcommitIndexlastAppliedStored x
A · leader1011001005
B · follower1001001005
C · follower1001001005

Step 2: A Sends AppendEntries

A sends this request to B and C. Delivery to C is delayed.

term: 7
prevLogIndex: 100
prevLogTerm: 7
entries: [{ index: 101, term: 7, command: SET x = 10 }]
leaderCommit: 100
FieldWhat the follower checks or learns
term: 7The sender is operating in election term 7. Reject a request older than the receiver’s current term.
prevLogIndex: 100The new entry follows index 100.
prevLogTerm: 7The receiver must have an entry at index 100 whose term is 7.
entriesThe entry to append after that matching position.
leaderCommit: 100The leader currently knows entries through 100 are committed.

term and prevLogTerm happen to match. A leader in term 7 could append after an entry created in term 6; then prevLogTerm would be 6.

Sending the request alone changes no replica state:

NodeLast log indexcommitIndexlastAppliedStored x
A · leader1011001005
B · follower1001001005
C · follower1001001005

Step 3: B Persists and Acknowledges

B has index 100 with term 7, so the preceding-entry check succeeds. It appends entry 101, persists the required log changes, and acknowledges replication through 101.

B does not apply the command: the request advertised leaderCommit = 100.

NodeLast log indexcommitIndexlastAppliedStored x
A · leader1011001005
B · follower1011001005
C · follower1001001005

The acknowledgement means entry 101 is durably stored on B, not that B has changed x. If the acknowledgement is lost, A can retry and B can confirm the existing entry.

Step 4: A Establishes Commitment

A receives B’s acknowledgement. Entry 101 now has durable copies on two of the three nodes: A and B. This is a majority. Since the entry belongs to A’s current term, A advances commitIndex to 101.

NodeLast log indexcommitIndexlastAppliedStored x
A · leader1011011005
B · follower1011001005
C · follower1001001005

Entry 101 is now committed, but no database value has changed in this schedule. B has not yet learned A’s new commit index. C is not required for this write to commit.

Step 5: A Applies and Responds

A applies entry 101 to its state machine. The storage engine changes x from 5 to 10, and lastApplied advances to 101. A returns success to the client.

NodeLast log indexcommitIndexlastAppliedStored x
A · leader10110110110
B · follower1011001005
C · follower1001001005

The write has succeeded even though both followers still store x = 5. A success response does not mean that every replica has applied the write.

Step 6: B Learns the Commit Index

A sends B another AppendEntries. It can be an empty heartbeat:

term: 7
prevLogIndex: 101
prevLogTerm: 7
entries: []
leaderCommit: 101

B verifies its entry at 101 and advances commitIndex. Before its application loop runs:

NodeLast log indexcommitIndexlastAppliedStored x
A · leader10110110110
B · follower1011011005
C · follower1001001005

B then applies entry 101:

NodeLast log indexcommitIndexlastAppliedStored x
A · leader10110110110
B · follower10110110110
C · follower1001001005

B learns commitment from A; it does not collect acknowledgements from other nodes.

Step 7: C Receives the Missing Entry

C cannot accept the heartbeat above because it lacks entry 101. A can instead send C a request anchored at its matching entry 100:

term: 7
prevLogIndex: 100
prevLogTerm: 7
entries: [{ index: 101, term: 7, command: SET x = 10 }]
leaderCommit: 101

This request carries both the missing entry and the commit notification. After C persists the entry and updates its commit index, but before applying it:

NodeLast log indexcommitIndexlastAppliedStored x
A · leader10110110110
B · follower10110110110
C · follower1011011005

After C applies the command:

NodeLast log indexcommitIndexlastAppliedStored x
A · leader10110110110
B · follower10110110110
C · follower10110110110

All three databases now contain x = 10. This is one possible schedule: A may notify followers before replying to the client, and followers may apply at different times.

One write schedule: A and B persist entry 101, A commits and applies it, B learns commitment, and C catches up later.
Replication, commitment, and application are separate events.

3. Read Path: Which Value Can Be Returned?

A read queries the local storage engine. It does not replay the Raft log to reconstruct x on every request.

Return to the state immediately after Step 5:

NodeLast log indexcommitIndexlastAppliedStored x
A · leader10110110110
B · follower1011001005
C · follower1001001005

The client has received success for SET x = 10 and now sends GET x. There are no subsequent writes in this example.

Reading a Follower Without Coordination

B’s database contains 5. Its log contains the new command, but B has neither learned commitment nor applied it. An explicitly stale follower read may therefore return 5.

For a linearizable read, this is insufficient: a read started after the successful write must reflect that write. B must establish a safe read position and apply through it before returning a value.

Reading Through the Leader

A currently stores 10, but simply calling itself leader is insufficient. A could have lost contact with the others while they elected a replacement.

A ReadIndex-style path works as follows. Assume the leader has already committed an entry from its current term, as A did in Step 4:

StepActionState or result
1A receives GET x and records a read index of 101.No log entry or database value changes.
2A confirms its leadership with a majority using a quorum exchange associated with the read.A plus B is sufficient; A cannot rely on an old acknowledgement.
3A waits until its lastApplied is at least 101.A is already at 101 in this example. If it were at 100, the read would wait.
4A reads x from its local storage engine.Returns 10.

A follower can also serve a linearizable read after obtaining a safe read index from the leader and applying through it. Leader leases are another approach, but require explicit clock and timing assumptions.

If A is isolated from B and C, it cannot complete the quorum check. It must wait or fail the read instead of returning an unchecked local value. The leadership and applied-position requirements are described in the Raft paper, Section 8.


4. Leader Election: Replacing A

A Fails Before C Catches Up

Branch from the write example after Step 5: A crashes, B has entry 101 but still believes only 100 is committed, and C has entries through 100.

NodeRoleCurrent termLast log entrycommitIndexStored x
ALeader, offline7index 101, term 710110
BFollower7index 101, term 71005
CFollower7index 100, term 71005

Followers normally receive AppendEntries heartbeats. Without valid leader communication, their election timers eventually expire. Suppose B times out first.

B Requests Votes in Term 8

B becomes a candidate, increments its term to 8, persists its vote for itself, and sends:

RequestVote:
  term: 8
  candidateId: B
  lastLogIndex: 101
  lastLogTerm: 7

Immediately after B starts the election:

NodeRoleCurrent termVote in current termLast log index
AOffline7Unchanged101
BCandidate8B101
CFollower7Unchanged100

C receives the request, updates its term to 8, and checks whether it has already voted in that term and whether B’s log is sufficiently up to date. C has not voted in term 8. Both last entries have term 7, and B’s index 101 is ahead of C’s 100, so C persists a vote for B.

After B receives C’s vote:

NodeRoleCurrent termVote in term 8Last log index
AOffline7None yet101
BLeader8B101
CFollower8B100

B has two votes out of three. The election changes roles and terms; it does not itself apply entry 101. B and C still store x = 5.

currentTerm and votedFor are durable. Otherwise, a node could vote, restart, and vote for another candidate in the same term. A node that observes a higher term updates its term and becomes a follower. Thus A steps down when it returns and learns of term 8.

Why C Cannot Win This Election First

If C had timed out first, its request would advertise last index 100, term 7. B would reject it because B has index 101 from the same term. With A offline, C could obtain only its own vote.

The general freshness rule compares the last entry’s term first, then its index:

candidate.lastLogTerm > voter.lastLogTerm
or (candidate.lastLogTerm == voter.lastLogTerm
    and candidate.lastLogIndex >= voter.lastLogIndex)

For example, a candidate ending at index 98 in term 8 is considered more up to date than a voter ending at index 101 in term 7. A longer log alone does not win. The candidate’s election term is a separate value: participating in newer elections does not make old log contents fresher.

How B Applies the Inherited Entry

Entry 101 was committed by A, but B has not learned that fact. B cannot commit an older-term entry merely by counting its copies. It can append a no-op entry in its own term:

101, term 7: SET x = 10
102, term 8: no-op

B sends C the missing entries. Once B and C durably store entry 102, B can commit that current-term entry and the preceding prefix through 101. It applies 101 and then the no-op; C follows after learning the commit index.

StageB: last log / committed / applied / xC: last log / committed / applied / x
B elected101 / 100 / 100 / 5100 / 100 / 100 / 5
B appends 102102 / 100 / 100 / 5100 / 100 / 100 / 5
C persists 101–102 and acknowledges102 / 100 / 100 / 5102 / 100 / 100 / 5
B commits and applies through 102102 / 102 / 102 / 10102 / 100 / 100 / 5
C learns commitment and applies102 / 102 / 102 / 10102 / 102 / 102 / 10

The previously successful write survives A’s crash even though neither surviving node had applied it before the election.

Split Votes

In the same three-node group, B and C may time out together and each vote for itself in term 8. With A offline, neither gets a majority. They retry in a later term with randomized election timeouts. If B’s next timeout expires first, it may collect C’s vote before C starts another election.

For a fixed group of N voters, a majority is floor(N / 2) + 1. Any two majorities overlap; one durable vote per term prevents two candidates from both winning that term. Preserving committed history additionally depends on the freshness and commitment rules above.


5. Repairing a Follower’s Log

A Follower Is Missing Entries

Consider a separate catch-up case in term 7. A has committed through 101; C has only entries through 98. Assume their entry 98 matches.

A: ... 98, 99, 100, 101
C: ... 98
StepRequest or resultC’s log
1A sends entry 101 with prevLogIndex = 100.Still ends at 98.
2C cannot find entry 100 and rejects the request.Unchanged; C does not leave a gap.
3A backs up to a matching position and sends entries 99–101 after index 98.C persists 99–101.
4That successful request also advertises leaderCommit = 101.C advances commitment through 101 and applies the entries in order.

The leader tracks two values per follower:

ValuePurpose
nextIndexNext entry the leader will try to send. It backs up after a prefix mismatch.
matchIndexHighest position confirmed replicated on that follower. Successful replies advance it.

The leader calculates quorum replication from this progress and its own log. It does not poll a separate quorum service. Conflict hints can reduce the number of retries.

A Follower Has Conflicting Entries

A mismatch can also mean that a follower has an uncommitted entry from an earlier leader. Each pair below is index:term; assume the current leader is operating in a later term than those shown:

Leader:             98:6  99:7  100:7  101:7
Follower:           98:6  99:6  100:6

A request expecting 100:7 fails even though the follower has index 100. After finding the match at 98:6, the follower replaces its conflicting suffix:

Follower repaired:  98:6  99:7  100:7  101:7

Only uncommitted conflicting entries can be replaced. This is why replicas apply commands after commitment: an applied command must not later disappear during log repair.

Bounding the Follower’s Commit Index

A follower must not commit beyond the matching prefix established by a successful AppendEntries. For contiguous entries, that prefix ends at prevLogIndex + len(entries):

if leaderCommit > commitIndex:
    commitIndex = min(leaderCommit, prevLogIndex + len(entries))

An empty heartbeat establishes a prefix only through prevLogIndex. A rejected request does not advance commitment. This prevents a newer commit notification from committing a missing entry or an unverified local suffix.


6. Crashes, Retries, and Snapshots

Where the Write Stops

Return to SET x = 10 at index 101 in term 7:

A crashes…What remainsWhat can happen next
Before appendingNo replica has entry 101.The client can retry against a new leader.
After only A persists 101An uncommitted entry on A.A later leader may retain or overwrite it.
After A and B persist 101A current-term entry on a majority.Future leaders preserve the committed write, even if A never announces commitment.
After applying, before replyingThe write has taken effect but the client has no success response.A retry can repeat the operation unless the application detects it.

The client cannot distinguish these cases from a timeout. For SET x = 10, repeating the command has the same effect in this isolated example. For INCREMENT x, replaying a new request could increment twice. Services use request IDs or client sequence numbers, with replicated deduplication state, to recognize retries.

Recovering Local Application State

The Raft log supports replicated ordering and recovery. A storage-engine WAL, when present, supports local recovery of the engine’s updates. Whether the integration uses both logs and when it flushes them are implementation choices.

A restart must recover a consistent pair: application state and the log position already reflected in that state. If the durable data includes an increment from index 101 but recovery incorrectly replays 101, the increment could be applied twice. A service can coordinate state and applied-position persistence atomically, or restore a snapshot and replay the committed suffix.

A successful write does not require every modified page or SSTable to have reached its final on-disk layout. It requires sufficient durable recovery information. Background flushes and compaction may continue later.

When Log Replay Is No Longer Available

Snapshots bound log growth. Suppose the application has applied through index 10,000 and safely stores:

lastIncludedIndex: 10000
lastIncludedTerm: <term of entry 10000>
application state: <state after applying through 10000>
cluster membership: <configuration at the snapshot boundary>

The service can then discard the covered log prefix. If C returns with entries only through 2,000, the leader can no longer send entries 2,001 onward from that deleted history:

StageC’s recovery state
Before catch-upLocal history reaches 2,000.
Install snapshotApplication state represents entries through 10,000.
Receive retained suffixStore entries 10,001 onward.
Learn commitment and applyAdvance from the snapshot boundary in order.

Snapshot creation, installation, and log truncation must preserve a consistent recovery boundary. Normal replication transfers commands; snapshot installation transfers a representation of application state.


7. Partitions and Safety

A Is Isolated from B and C

A cannot obtain a majority for new writes or complete quorum-confirmed reads. B and C can elect a new leader and continue. A may still believe it is leader until it learns the newer term, but that belief does not allow it to commit entries alone.

With fewer than two reachable voters in this three-node group, new writes stop. Raft preserves committed history rather than allowing both sides of a partition to commit conflicting operations.

Protocol Guarantees

PropertyGuaranteeMechanism
Election SafetyAt most one elected leader per term.Intersecting majorities and one durable vote per term.
Leader Append-OnlyLeaders append new entries rather than overwrite their own log during replication.Conflicting suffixes are repaired on followers; snapshot compaction removes covered prefixes separately.
Log MatchingEqual index and term identify the same command and preceding prefix.AppendEntries checks the preceding index and term.
Leader CompletenessSubsequent leaders preserve committed entries.Election freshness, quorum intersection, and the current-term commitment rule.
State Machine SafetyReplicas cannot apply different commands at the same index.Log safety and ordered application of committed entries.

For example, after one replica applies SET x = 10 at index 101, another cannot apply DELETE x at that index. It may temporarily be behind, but it cannot choose a different committed command.


8. Operational Limits

ConcernConsequence
Leader throughputOne leader coordinates writes for a group. Databases can partition data across multiple Raft groups.
Write latencyIncludes log persistence and replication to enough followers for a majority, followed by local application and response. Slow minority replicas need not block the write.
Batching and pipeliningAmortize persistence and network work without removing durability or quorum requirements.
Disk and scheduling stallsSlow fsyncs, garbage collection, CPU starvation, or overload can delay heartbeats and trigger elections.
Election timeoutsShorter timeouts detect failures faster but are more sensitive to normal latency variation. Elections temporarily interrupt writes.
Membership changesChanges must preserve safe quorum overlap. Joint consensus uses majorities of both old and new configurations during transition.
Service integrationRequires validation, deduplication, read policies, snapshots, backups, monitoring, repair, and client retry handling.

Fault-Model Boundary

Raft assumes a follower truthfully acknowledges durable storage. It does not handle a node that lies about its log or sends conflicting fabricated messages.

Byzantine fault-tolerant protocols address arbitrary or malicious behavior. A common BFT model requires 3f + 1 replicas to tolerate f Byzantine faults: four for one fault, seven for two. That stronger model generally requires additional communication and verification. Examples include Tendermint/CometBFT, HotStuff-style protocols, and SmartBFT-style systems, used where mutually distrustful participants or compromise are part of the threat model.


9. Revision Summary

  • Stored: an entry exists in a node’s durable log.
  • Committed: the protocol has established that the entry must be preserved; a follower learns this through leaderCommit.
  • Applied: the node has executed the committed command against its local state machine.
  • Read: query applied state, with leadership and application-progress checks when linearizability is required.
  • Election: choose a new leader through a majority, constrained by log freshness.
  • Recovery: repair an uncommitted suffix, replay retained entries, or install a snapshot.

The write, election, and recovery rules follow the Raft paper. The example schedules separate events for clarity; implementations may batch or overlap them while preserving the same ordering constraints.

Leave a Comment