Skip to main content

Read Replicas — Scale Reads Without Scaling Writes

Route read queries to async copies of your primary DB — scale read throughput without affecting write capacity.

When to use

  • Read-heavy workloads (>80% reads)
  • Analytics or reporting queries that shouldn't impact primary
  • Reducing primary load for latency-sensitive operations

Tradeoffs

  • Replication lag = stale reads (can be milliseconds to seconds)
  • Replica promotion on failover takes time; clients need routing awareness
var (
primaryDB *sql.DB
replicaDB *sql.DB
)

type OpType int
const (Write OpType = iota; Read)

func DBFor(op OpType) *sql.DB {
if op == Write {
return primaryDB
}
return replicaDB
}

func CreateUser(ctx context.Context, u *User) error {
_, err := DBFor(Write).ExecContext(ctx, "INSERT INTO users ...", u.ID, u.Name)
return err
}

func GetUser(ctx context.Context, id string) (*User, error) {
row := DBFor(Read).QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
return scanUser(row)
}

Gotcha: Never read from a replica immediately after a write you depend on. Replication lag can be non-trivial. Use read-after-write consistency routing: send to primary if the client just wrote.