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

22 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

To understand how Raft processes a write, a node is separated into two independent components:

  1. The Consensus Engine (Raft Log): Responsible for replicating ordered log entries across nodes and establishing durable cluster consensus.
  2. The Application Engine (State Machine): The database or key-value store that executes committed log entries to produce final user-visible data (e.g., setting x = 10).
[ Client Request ] ──► [ Raft Log Engine ] ──(Consensus)──► [ State Machine ] ──► [ DB Disk ]
│                                   │
lastLogIndex                        commitIndex / lastApplied

The Three Progress Indices

Every node tracks three monotonic sequence numbers to manage this pipeline:

  • lastLogIndex (Log Horizon): The index of the highest entry appended to the node’s local Raft log. It answers: “What is written to the local log file on disk?”
    • Note: Uncommitted entries at lastLogIndex may be overwritten or discarded if a leader changes.
  • commitIndex (Safety Horizon): The highest log entry that Raft guarantees is permanent because a majority of nodes have persisted it. It answers: “What has the cluster safely agreed upon?”
    • Note: Driven by the leader’s consensus quorum. commitIndex strictly moves forward.
  • lastApplied (Execution Horizon): The highest log entry whose operation has been executed against the local state machine/database. It answers: “How far has my local database state caught up?”

Because database application runs asynchronously in the background, a entry progresses through three explicit stages:

StageSequence ConditionLocal State Effect
1. AppendedlastLogIndex = 101, commitIndex = 100Logged on disk, but uncommitted. Database still holds old value (x = 5).
2. CommittedcommitIndex = 101, lastApplied = 100Cluster agreement reached. Safe to execute, but database still holds old value (x = 5).
3. AppliedlastApplied = 101Command executed by state machine. Database updated (x = 10).

The application loop runs continuously in the background, ensuring lastApplied eventually reaches commitIndex:

while lastApplied < commitIndex:
    entry = log[lastApplied + 1]
    apply_to_database(entry.command)
    lastApplied += 1

Starting State

All three nodes (A, B, C) start fully synchronized at index 100 in Term 7:

NodelastLogIndexcommitIndexlastAppliedStored x
A (Leader)1001001005
B (Follower)1001001005
C (Follower)1001001005

Client sends SET x = 10 to Leader A.


Step 1: Leader Appends to Local Log

Leader A writes the command to its local Raft log at index 101.

lastLogIndex advances on A, but commitIndex remains 100 because no follower has received it yet.

NodelastLogIndexcommitIndexlastAppliedStored x
A (Leader)1011001005
B (Follower)1001001005
C (Follower)1001001005

Step 2: Leader Replicates to Followers

A sends AppendEntries RPC to followers B and C containing entry 101. Message to C is delayed in the network.

Follower B appends entry 101 to its local log and returns a success response to A. B does not execute the write yet because leaderCommit in the RPC is still 100.

NodelastLogIndexcommitIndexlastAppliedStored x
A (Leader)1011001005
B (Follower)1011001005
C (Follower)1001001005

Step 3: Leader Reaches Quorum and Commits

Leader A receives B’s ACK. Entry 101 is now stored on a majority of nodes (A and B).

A advances its commitIndex to 101. The write is now durable and committed cluster-wide.

NodelastLogIndexcommitIndexlastAppliedStored x
A (Leader)1011011005
B (Follower)1011001005
C (Follower)1001001005

Step 4: Leader Applies Command and Responds

Leader A’s background application loop sees lastApplied (100) < commitIndex (101). It executes SET x = 10 against its database, advances lastApplied to 101, and responds to the client.

NodelastLogIndexcommitIndexlastAppliedStored x
A (Leader)10110110110
B (Follower)1011001005
C (Follower)1001001005

Step 5: Follower Catches Up

On the next heartbeat or subsequent RPC, Leader A sends leaderCommit = 101 to B.

B advances commitIndex to 101, triggers its application loop, advances lastApplied to 101, and updates its database state to x = 10.

NodelastLogIndexcommitIndexlastAppliedStored x
A (Leader)10110110110
B (Follower)10110110110
C (Follower)1001001005
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