CockroachDB uses a distributed consensus protocol (Raft) for writes, which inherently involves cross-region communication and thus latency. Follower reads, however, allow read-only operations to be served from a replica in the same region as the client, bypassing the Raft leader and significantly reducing read latency.

Let’s see this in action. Imagine we have a CockroachDB cluster spanning us-east-1 and eu-west-1.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/cockroachdb/cockroach-go/v2/crdb"
	"github.com/jackc/pgx/v4"
)

func main() {
	// Connection string for a client in us-east-1
	// Assuming your cluster has nodes in us-east-1 and eu-west-1
	connStr := "postgresql://root@localhost:26257/defaultdb?sslmode=disable&application_name=follower-read-client"

	conn, err := pgx.Connect(context.Background(), connStr)
	if err != nil {
		log.Fatalf("Unable to connect to database: %v\n", err)
	}
	defer conn.Close(context.Background())

	// --- Scenario 1: Standard Read (potentially cross-region) ---
	fmt.Println("--- Performing Standard Read ---")
	startTime := time.Now()
	var count int
	err = conn.QueryRow(context.Background(), "SELECT count(*) FROM users").Scan(&count)
	if err != nil {
		log.Fatalf("Standard read failed: %v\n", err)
	}
	duration := time.Since(startTime)
	fmt.Printf("Standard read complete. Count: %d, Duration: %s\n", count, duration)
	// Note: If the client is in us-east-1 and the Raft leader for this data
	// is in eu-west-1, this read will incur cross-region latency.

	// --- Scenario 2: Follower Read ---
	fmt.Println("\n--- Performing Follower Read ---")
	// This is the key: crdb.WithFollowerReads()
	ctxWithFollowerRead := crdb.WithFollowerReads(context.Background())
	startTime = time.Now()
	err = conn.QueryRow(ctxWithFollowerRead, "SELECT count(*) FROM users").Scan(&count)
	if err != nil {
		log.Fatalf("Follower read failed: %v\n", err)
	}
	duration = time.Since(startTime)
	fmt.Printf("Follower read complete. Count: %d, Duration: %s\n", count, duration)
	// Note: This read will attempt to serve from a replica in the *same* region
	// as the client, significantly reducing latency if the client is in us-east-1.
}

This Go program demonstrates the difference. The first query, SELECT count(*) FROM users, is a standard read. By default, CockroachDB might route this to the Raft leader for the data’s range, which could be in another region. The second query uses crdb.WithFollowerReads(context.Background()). This special context tells CockroachDB to prioritize serving the read from a follower replica located in the same region as the client connection. The difference in Duration will be stark if your client is geographically distant from the Raft leader.

The problem follower reads solve is the inherent latency introduced by CockroachDB’s distributed nature. Every write transaction, to maintain strong consistency across all replicas, must go through a consensus protocol like Raft. Raft requires a majority of replicas to acknowledge a write before it’s committed. If your cluster spans multiple regions, this means writes often involve round trips between continents, leading to noticeable latency. Reads, too, can be affected if they need to consult the Raft leader for the most up-to-date information. Follower reads provide a mechanism to relax this strict consistency for read operations only, allowing them to be served by a local replica that is only slightly behind the leader.

Internally, when a client requests a follower read, CockroachDB looks for a replica of the requested data’s range that is in the same region as the client’s connection. If such a replica exists, the query is sent directly to that replica. This replica will serve the read based on its local data, which is guaranteed to be no more than a certain amount of time (typically a few seconds, configurable via follower_read_timestamp_refresh_interval) behind the Raft leader. The client receives the data much faster because the network round trip is confined to a single region.

The primary lever you control for follower reads is the crdb.WithFollowerReads() option in the Go driver (or its equivalent in other drivers/clients). You can apply this selectively to read operations where slightly stale data is acceptable. The actual performance gain is directly proportional to the geographical distance between your client’s region and the region where the Raft leader for that data resides. For applications with users distributed globally, enabling follower reads on reads that don’t require absolute real-time data can dramatically improve user experience.

The follower_read_timestamp_refresh_interval cluster setting dictates how often a follower replica will refresh its read timestamp from the Raft leader. A shorter interval means reads will be closer to strongly consistent but might incur slightly more overhead. A longer interval increases potential staleness but reduces overhead.

The one thing most people don’t realize is that follower reads are not a magical "always faster" button. They are a trade-off: reduced latency for potentially slightly stale data. CockroachDB’s crdb.WithFollowerReads context actually injects a specific SQL hint into the query: AS OF SYSTEM TIME follower_read_timestamp(). This tells the database engine to execute the query using the timestamp of the local follower replica, rather than waiting for the global USABLE timestamp provided by the Raft leader. The follower_read_timestamp() function internally queries the system catalog to find the latest timestamp known to the local replica, which is then used for the read.

After successfully implementing follower reads and seeing your read latencies drop, the next thing you’ll likely encounter is managing the staleness of the data being read.

Want structured learning?

Take the full Cockroachdb course →