Introduction

As a backend engineer who has spent many years working at AWS and dealing with DynamoDB every day, whenever I use the ConsistentRead=true parameter, I cannot help but wonder: what exactly gives a distributed database the ability to promise a “strongly consistent read”? If a write request has already returned successfully, what guarantees that a subsequent read request will definitely see it, rather than reading stale state from an old leader or a lagging replica? Behind this is in fact a core problem faced by all systems based on replicated state machines: how do we find a valid linearization point for a read request?
Using the Raft consensus algorithm, which is easier to reason about and to implement by hand, this article starts from the definition of linearizability and derives the exact constraints required for linearizable reads. It also explains what problems are solved by optimizations such as ReadIndex, leader lease read, and follower read, and provides a reference Go implementation (commit).
Constraints Imposed by Linearizability
In Raft, the most straightforward linearizable read implementation is to write Get into the Raft log as a log entry as well. The correctness of this approach is easy to prove: the read operation itself has a clear position in the log, and when that entry is committed and applied in order, that log index is the linearization point of the read. But the cost of this approach is that read requests go through the full replication, commit, and apply pipeline, introducing extra RPC and IO overhead.
The question is: if we do not want to save read operations into the Raft log, can we still preserve linearizability? To answer that, we first need to go back to the definition of linearizability.
For a read request R, from the perspective of an external observer, it has an invocation start time R.invoke and a response time R.response. Linearizability requires that we be able to choose a logical linearization time point T for R between those two moments:
R.invoke ------------- T ------------- R.response
The result returned by the read corresponds to the state of the system at time T. Clearly, T cannot be chosen arbitrarily within that interval. At that moment, the visibility of other writes to read request R must satisfy the constraints observable from the outside:
- Writes that completed and returned before
R.invokeare entirely earlier than this read in real time (with no overlap), so they must be visible to this read; otherwise the read would return stale state. - Writes that start only after
R.responseare entirely later than this read on the real-time axis, so they cannot affect the return value of this read. This is obviously true, because you cannot read a value that is only written in the “future.” - Writes whose time interval overlaps with
Rmay in actual occurrence be ordered either beforeTor afterT; as long as there exists a global serial order that explains the return values of all observed operations, linearizability allows this freedom. It is therefore entirely valid for the read to observe the values written by them.- This also means we are free to delay the return time of the read and allow it to observe more writes that overlap with it, without violating linearizability. This observation is what makes later batching optimizations possible.
In Raft's replicated state machine model, the state is defined by the log prefix that has already been applied: suppose a read request chooses ReadIndex = i, then what it reads is the result of the state machine after executing log[0..i]. This means that in a concrete implementation we can use ReadIndex to define the linearization time point T.
Returning to the constraint above, a read must observe all writes that completed before it began. Therefore, for read request R, there is in theory a minimum visibility boundary L:
L = the highest log index among all writes that completed and returned before R.invoke
A linearizable read only needs to satisfy:
ReadIndex >= L
It does not need to be exactly equal to L. If ReadIndex is later than L and includes any number of newly added writes that overlap with R, that is still fine, because those writes can be safely linearized before T; it only increases the latency of the read request.
In practice, we also have no way to capture the exact state L at the instant the read request arrives. In a distributed system, that is difficult to record directly. What we can do instead is choose a more conservative, later ReadIndex position, as long as it covers all writes that must be observed.
So, starting from the definition of linearizability, we can transform the requirement into:
- After receiving a read request, choose a
ReadIndexposition that is sufficiently safe, so that it covers at least all writes that have already completed beforeR.invoke, i.e.ReadIndex >= L - Ensure the local state machine has already applied up to that position, and only then perform the read
From Theoretical Requirements to a Raft Implementation
After the theoretical analysis above, we need to map it onto a concrete Raft implementation. The problem is that the L we defined earlier is only a semantic boundary, not state maintained by the Raft algorithm. What each node can observe locally is only its own CommitIndex. So we need to understand under what conditions we can ensure CommitIndex >= L.
Whose CommitIndex Is Qualified to Serve as ReadIndex?
First, the write path in Raft is always driven by the leader. Write requests first enter the leader's log, then the leader replicates them to a majority; only when the leader confirms that some log prefix has been accepted by a majority does it advance CommitIndex, and then it notifies followers of this progress through subsequent AppendEntries RPCs.
Therefore, the leader is the only node that can proactively determine the committed frontier. A follower only passively receives logs and leaderCommit progress. Its local CommitIndex may lag arbitrarily far behind due to network delay and partitions. Even if a lagging follower's local state is entirely self-consistent, it still cannot prove that it already includes all writes that completed before the read request began.
When Does CommitIndex >= L Hold?
When a node receives a read request, as long as it believes itself to be the leader, one intuitive idea is to directly choose:
ReadIndex = Raft.CommitIndex
This seems to be a safe position that is more conservative and later than L, right? Then we just wait until the state machine has applied up to that position and read locally. The problem with this idea is that the local CommitIndex is only this node's view of the committed frontier. Whether it is safe enough depends on additional conditions.
Is this node really the current valid leader?
If it has already been isolated by a network partition and the majority has elected a new leader, then the new leader may already have committed new writes. The old leader's local CommitIndex will lag behind the system's true committed frontier. In that case, if the old leader directly uses its own CommitIndex as ReadIndex to handle a newly arrived read, it may miss writes that have already completed elsewhere.
If this node has just become leader, it must ensure that its committed frontier is fresh enough
The Raft Leader Completeness property guarantees that a valid new leader's log contains all committed entries. But containing those entries ≠ knowing they are committed — when a new leader has just been elected, its CommitIndex may still be stale, because elections do not check that state.
It is easy to construct an intuitive counterexample here: suppose some leader has just advanced its own CommitIndex and applied it, so the corresponding write has successfully returned, but it crashes before it has had time to broadcast that progress. No matter who the new leader is, it will hold the old CommitIndex that has not yet been advanced. If a read request now lands on the new leader and we choose that stale CommitIndex as ReadIndex, we will miss writes that had already completed on the old leader.
Safety Guarantee 1️⃣: Confirming the Committed Frontier
Raft has a key rule (Figure 8): a new leader cannot directly commit an old-term entry merely because that entry has already been replicated to a majority. The leader can only advance commit by committing an entry from the current term; once some entry from the current term is committed, all prior logs before it can also be regarded as committed.
Therefore, when first elected, a new leader can append a NOP entry from the current term and replicate it to a majority. Once that NOP is committed, the leader can safely advance CommitIndex to the NOP's position. All entries before that position, including entries from old terms that may already have completed but that the new leader was not yet explicitly aware were committed, are now included in the known committed frontier.
This operation can be understood as:
Current-term NOP commit
=> the current leader has already committed a log entry in this term
=> it can safely confirm a committed log prefix
=> ReadIndex can be constructed based on this log prefix
Safety Guarantee 2️⃣: Majority Confirmation of Leader Validity
Having a reliable committed frontier is still not enough. The leader must also prove that it has not already been replaced by a new leader. But if you are familiar with Raft, an intuitive doubt arises immediately: this is fundamentally impossible to prove. Confirmation from a majority can only prove that the leader identity was valid during some interval in the past; it cannot prove that it is still valid when the replies are received.
In fact, we do not need to care whether the node is still leader at the instant the replies are received. What we care about is this: as long as there existed some time point within the interval [R.invoke, R.response] at which it was confirmed by a majority as the leader, then the CommitIndex corresponding to that moment can serve as a legal linearization point, and that log prefix must include all writes that completed and returned before R.invoke — because being confirmed by a majority as leader implies that those committed writes are not missing. Even if a new leader begins committing new writes after that, those writes cannot possibly have completed before R.invoke; since their time intervals overlap, it is entirely legal for them to be invisible to R, and we can linearize them after R.
The approach in the Raft paper is: when handling a read request, the leader sends heartbeats to followers and waits for replies from a majority. If the majority still accepts its heartbeat and does not return a higher term, then this proves that during the interval between issuing the RPC and receiving majority confirmation, there must have been a time window in which it was the legitimate leader.
Although we cannot know exactly what CommitIndex corresponded to that time window, the cleanest engineering approach is simple: after receiving majority confirmation, the leader directly reads the current CommitIndex as ReadIndex. This is a more conservative and later position, and therefore it naturally also guarantees CommitIndex >= L.
Safety Guarantee 3️⃣: Waiting for the State Machine
Raft.CommitIndex only means that certain log entries have been committed, but state machine application is asynchronous and may lag behind. So even if the leader has obtained a safe ReadIndex, it still cannot read the local state machine immediately; it must wait until state machine application catches up. The difference here is:
ReadIndexindicates which writes should be visibleLastApplied >= ReadIndexis what proves that the local state machine has actually seen those writes take effect
Lease Read Optimization
The idea of lease read is to stop requiring every read request to perform a majority confirmation. Instead, a lease window is established through heartbeats. At some time t0, the leader sends heartbeats to followers and proves its legitimacy through majority confirmation. Since the majority followers reset their election timeout after receiving the heartbeat, within a sufficiently short lease window they definitely will not vote for another candidate.
Any new node becoming leader requires votes from a majority, and any two majorities must intersect. Therefore, it can be proven that during the old leader's lease window, no new leader can be elected. In other words, the legitimacy of the current leader lasts until:
leaderLeaseUntil = t0 + leaseDuration
Note that this does not require globally synchronized clocks. Correctness depends only on the weaker assumption of bounded clock drift: it is enough that the local monotonic clocks of the leader and followers do not diverge too much over a short period. More specifically:
leaseDuration < minElectionTimeout - clockDriftBound - timerSchedulingMargin
When a read request arrives, if the current term already has a committed entry, then the leader can read directly from its local state without sending RPCs to a majority. The essence of this optimization is that it replaces per-read majority confirmation with a proof of leader validity over a time window, thereby greatly reducing RPC overhead.
Follower Read and Batching Optimization
If we want to implement safe reads from a follower, we can in fact ask the leader for a certified safe ReadIndex; the follower only performs the local read after obtaining that ReadIndex and waiting for its own state machine to replicate and apply up to that position. The benefit is that even if the client request first hits a follower, it does not need to be redirected to the leader, making the processing path more natural.
At its core, there is no way to avoid having the leader generate the ReadIndex. But on top of that, we can add batching optimization and trade latency for throughput. Because multiple read requests arriving within a short period can actually share the same ReadIndex, there is no need for each request to send a separate RPC to the leader. A more efficient approach is to first collect read requests arriving within a short time window into a batch, then obtain a single ReadIndex for them, and once the state machine has applied up to that position, process that whole batch at once. This does not change the semantics of linearizability, but it can significantly reduce extra RPCs and use read replicas to drive up total system throughput.
In practice, systems like DynamoDB do not need to support follower read. An important reason is that they already offer different levels of read semantics. For high volume of simple KV queries, most applications accept eventually consistent reads rather than requiring strict linearizability every time; in that case, read requests can be handled directly by replicas, with no need to coordinate with the leader for an extra safe ReadIndex, and no need for batching optimization either. In other words, batching optimization mainly suits requirements like “we want to use read replicas to share the load, but we also want to preserve linearizability.” That is not especially meaningful for simple KV stores where reads are cheap, but it is valuable for distributed SQL systems like TiDB, where complex read queries can be much more expensive.
Applications in System Design Interviews
A deep understanding of Raft linearizable reads helps us build the correct mental model in system design interviews when discussing high availability, read replicas, and strong consistency requirements. It helps avoid letting the interviewer's follow-up questions drift in the wrong direction, and it is also a high-value signal of technical depth.
Once a system requires strong consistency guarantee, we cannot simply mention “multiple replicas,” “synchronous replication,” or “read/write majorities” and stop there. We must explain how a completed write request becomes an irreversible fact, and how a read request proves that it has observed those completed writes. The correct design space here is actually quite limited:
- Single-primary reads and writes: all strongly consistent reads and writes go through the primary. This keeps the semantics simple, but the primary is obviously a single bottleneck.
- Fully synchronous replication: before a write returns, ensure that all replicas that support strongly consistent reads have already synchronized, so any replica can serve reads directly. The cost is that both write latency and availability are constrained by the slowest replica.
- It's important to note that the set of synchronous replicas must be fixed, unlike Kafka which maintains dynamically changing In-Sync Replicas for partial synchronous replication. Otherwise, linearizable reads still cannot be directly guaranteed—because read requests could be routed to nodes that already removed from ISR.
- We can mitigate this by implementing some fencing mechanism to first disable any read requests being routed to nodes that we plan to remove from ISR; but during this time period system will be unavailable.
- Consensus replication: writes only need confirmation from a majority, and reads can also use various techniques to optimize throughput. The cost is native integration with complex consensus protocols such as Raft / Paxos.
This classification is very useful in interviews. For example, when designing a used-car marketplace, if the core consistency requirements are concentrated only in a small number of order-related transactional operations, then using PostgreSQL with synchronous replication, plus an external HA control plane and an explicit failover mechanism, is usually already sufficient.
But if the scale of the problem rises to a large trading platform, inventory/order/payment systems, cross-partition transactions, or strongly consistent metadata management, then ordinary synchronous replication may no longer be the most suitable abstraction. The core tradeoff here is not how many RPCs are sent as the number of replicas increases, but rather whether the write path is determined by the slowest replica. In that case, if we want to avoid making the write path wait for the slowest replica while still requiring linearizable reads, then we must introduce systems whose underlying implementation already includes a consensus protocol — that is, natively distributed storage systems such as DynamoDB, Spanner, TiDB, and etcd.
Conclusion
The easiest way to overcomplicate Raft linearizable reads is to instinctively interpret them as always reading the globally latest state. But if we start from the definition of linearizability and derive the full chain step by step, we find that this is actually a very weak constraint:
It is enough not to miss writes that completed before the read request began; for writes that overlap with the read request, whether they are observed or not does not violate linearizability.