# Dragonboat Raft Protocol: How ReadIndex Enables Linearizable Reads

> Discover how Dragonboat uses the Raft ReadIndex protocol for linearizable reads. Ensure strong consistency by applying Raft log entries to your local state machine.

- Repository: [lni/dragonboat](https://github.com/lni/dragonboat)
- Tags: internals
- Published: 2026-03-06

---

**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`](https://github.com/lni/dragonboat/blob/main/raftpb/types.go), which declares the `ReadIndex` and `ReadIndexResp` structures used for communication between nodes:

```go
// 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`](https://github.com/lni/dragonboat/blob/main/nodehost.go). The `ReadIndex` function initiates the protocol, while `ReadLocalNode` performs the actual state machine query after the index is confirmed:

```go
// 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`](https://github.com/lni/dragonboat/blob/main/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:

```go
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`](https://github.com/lni/dragonboat/blob/main/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`](https://github.com/lni/dragonboat/blob/main/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:

```go
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`](https://github.com/lni/dragonboat/blob/main/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`](https://github.com/lni/dragonboat/blob/main/node.go) (event loop handling), [`request.go`](https://github.com/lni/dragonboat/blob/main/request.go) (pending request tracking), and [`queue.go`](https://github.com/lni/dragonboat/blob/main/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`](https://github.com/lni/dragonboat/blob/main/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`](https://github.com/lni/dragonboat/blob/main/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.