Dragonboat Raft Protocol: How ReadIndex Enables Linearizable Reads

Dragonboat implements the Raft ReadIndex protocol to provide linearizable reads without writing to the Raft log, ensuring strong consistency by verifying the local state machine has applied all entries up to the leader's current commit index.

Dragonboat is a high-performance Go implementation of the Raft consensus algorithm. When applications require linearizable reads—guaranteeing that read operations reflect all previously completed writes—the library leverages a specific Raft protocol optimization known as ReadIndex.

What is the ReadIndex Protocol in Raft?

The ReadIndex protocol is a mechanism defined in the original Raft paper that allows processing read requests without appending entries to the replicated log. Instead of treating reads as write operations that require consensus, the protocol works by having the leader confirm that the local state machine has applied all entries up to the current commit index before executing the read. This eliminates the I/O overhead of log replication while maintaining linearizability—the guarantee that a read returns the most recent committed write.

How Dragonboat Implements the ReadIndex Protocol

Dragonboat’s implementation spans multiple source files, separating message definitions, public APIs, and internal request handling.

Message Types and Core Structures

The protocol’s message types are defined in raftpb/types.go, which declares the ReadIndex and ReadIndexResp structures used for communication between nodes:

// Message types for ReadIndex protocol
ReadIndex     MessageType = 16
ReadIndexResp MessageType = 17

These messages carry the read request from the client to the leader and return the commit index that must be applied before the read executes.

Public API: Initiating Linearizable Reads

Applications interact with the protocol through NodeHost methods defined in nodehost.go. The ReadIndex function initiates the protocol, while ReadLocalNode performs the actual state machine query after the index is confirmed:

// Initiate linearizable read
rs, err := nh.ReadIndex(shardID, time.Second)
if err != nil {
    log.Fatalf("ReadIndex failed: %v", err)
}

// Wait for confirmation that commit index is applied
<-rs.ResultC

// Execute read against local state machine
value, err := nh.ReadLocalNode(rs, query)

The ReadIndex call returns immediately with a RequestState, but the read only proceeds after the ResultC channel closes, indicating the local node has applied all entries up to the leader’s commit index.

Internal Request Processing

Inside the node’s event loop (node.go), the handleReadIndex method processes incoming read requests. It adds the request to a pending queue and invokes the transport layer’s ReadIndex RPC to contact the leader:

func (n *node) handleReadIndex(req *readIndexReq) {
    // Add to pending queue
    n.pendingReadIndex.add(req)
    // Send RPC to leader
    n.transport.ReadIndex(req.target, req.readIndexCtx)
}

The pendingReadIndex structure in request.go tracks outstanding requests, manages timeouts, and completes the request once the leader’s ReadIndexResp arrives and the associated commit index has been applied locally.

Request Batching and Queueing

For performance optimization, Dragonboat batches read requests using the readIndexQueue defined in queue.go. When multiple ReadIndex calls occur in close temporal proximity, the queue buffers them into a single batch, reducing RPC overhead while maintaining linearizability for each individual read.

Code Example: Performing a Linearizable Read in Dragonboat

The following complete example demonstrates the recommended pattern for executing linearizable reads:

package main

import (
    "log"
    "time"
    
    "github.com/lni/dragonboat/v4"
)

func performLinearizableRead(nh *dragonboat.NodeHost, shardID uint64, query []byte) {
    // 1) Issue a ReadIndex request – this returns a RequestState immediately.
    rs, err := nh.ReadIndex(shardID, time.Second)
    if err != nil {
        // handle timeout / network error
        log.Fatalf("ReadIndex failed: %v", err)
    }

    // 2) Wait for the request to be completed (ResultC is closed when ready).
    <-rs.ResultC // blocks until the read index has been applied

    // 3) Perform a local state‑machine query with linearizability guarantee.
    value, err := nh.ReadLocalNode(rs, query)
    if err != nil {
        log.Fatalf("ReadLocalNode failed: %v", err)
    }
    fmt.Printf("Linearizable read result: %v\n", value)
}

This pattern ensures that the state machine query executes only after the node has applied all log entries up to the commit index acknowledged by the leader.

ReadIndex vs. Stale Reads: Performance Trade-offs

Dragonboat offers two distinct read modes:

ReadIndex Protocol (NodeHost.ReadIndex + ReadLocalNode): Provides linearizable consistency by verifying the local state machine is up-to-date with the leader’s commit index. This requires network communication with the leader and waiting for local application of the target index, adding latency but guaranteeing strong consistency.

Stale Reads (NodeHost.StaleRead): Bypasses the ReadIndex protocol entirely, querying the local state machine immediately without verifying commit index alignment. This offers lower latency and higher throughput but may return data that does not reflect recent committed writes, providing only eventual consistency.

Choose the ReadIndex protocol when data freshness is critical; use StaleRead for high-throughput scenarios where slightly outdated data is acceptable.

Summary

  • Dragonboat implements the Raft ReadIndex protocol to provide linearizable reads without appending log entries.
  • The protocol uses ReadIndex and ReadIndexResp message types defined in raftpb/types.go to coordinate between clients and the leader.
  • Applications use NodeHost.ReadIndex to initiate requests and ReadLocalNode to execute queries after the commit index is verified.
  • Internal implementation spans node.go (event loop handling), request.go (pending request tracking), and queue.go (request batching).
  • The ReadIndex protocol trades slightly higher latency for strong consistency compared to StaleRead, which offers eventual consistency only.

Frequently Asked Questions

Does Dragonboat use log replication for linearizable reads?

No. Dragonboat uses the ReadIndex protocol, which avoids writing read requests to the Raft log. Instead, the leader confirms its current commit index to the client, who then waits for its local state machine to apply all entries up to that index before executing the read. This eliminates the I/O overhead of log replication while maintaining linearizability.

What is the difference between ReadIndex and LeaseRead in Raft?

ReadIndex requires the leader to communicate its commit index to the client (or follower) and wait for local application, ensuring linearizability even if the leader changes. LeaseRead (not implemented in Dragonboat) relies on the leader maintaining a time-based lease during which it assumes no other node can become leader, allowing local reads without network round-trips. LeaseRead offers lower latency but requires clock synchronization and provides weaker guarantees during leader transitions.

How does Dragonboat handle ReadIndex failures or timeouts?

When NodeHost.ReadIndex is called, it accepts a timeout parameter (e.g., time.Second). If the leader does not respond within this window, or if the node loses leadership during the request, the function returns an error. Internally, pendingReadIndex in request.go tracks request lifecycles and expires entries that exceed their deadline, preventing indefinite blocking and allowing clients to retry.

Can ReadIndex requests be batched for better performance?

Yes. Dragonboat automatically batches ReadIndex requests using the readIndexQueue structure in queue.go. When multiple read requests arrive in close temporal proximity, the queue buffers them into a single batch that shares one network round-trip to the leader. Each request in the batch maintains its individual linearizability guarantee, but the batching reduces RPC overhead and improves throughput under high read concurrency.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →