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
- Go
- Python
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)
}
from enum import Enum
class OpType(Enum):
READ = "read"
WRITE = "write"
def db_for(op: OpType):
return primary_db if op == OpType.WRITE else replica_db
def create_user(user: dict):
db_for(OpType.WRITE).execute(
"INSERT INTO users (id, name) VALUES (%s, %s)",
(user["id"], user["name"])
)
def get_user(user_id: str) -> dict:
return db_for(OpType.READ).query(
"SELECT * FROM users WHERE id = %s", (user_id,)
)
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.