BitBootcampBitBootcamp ← all explainers 🔄 Review ⚙ Settings

Pattern — Read-Replica Lag Handling

The problem it solves

The clean fix for Drill 79 — one unindexed report holding all twenty primary connections while payments time out — is to move the heavy reads off the primary entirely, onto a read replica. But a replica is asynchronous: it applies the primary's writes seconds behind, so every read you offload can now return stale data. The report from the replica can miss the payment that landed two seconds ago; worse, if you also route balance lookups there to scale, a customer who just paid refreshes and sees the old balance — a read-your-writes violation that reads as a bug, a double-payment, or a support ticket. Offloading reads without handling lag trades a pool-exhaustion outage for a silent-correctness outage.

The pattern

Read-replica lag handling makes replica offload safe by treating replication lag as a first-class, measured quantity and routing each read by the freshness it actually needs. Three moves together: measure lag continuously and evict any replica that falls too far behind; classify each read by how stale it may legally be (a report tolerates 30s; a just-written balance tolerates 0); and give the reads that need it read-your-writes consistency — pin them to the primary, or to a replica proven caught up past the write — so the reader never sees older data than it already wrote.

How to apply it

  1. Measure lag as an SLI, in real units, per replica. In PostgreSQL, now() - pg_last_xact_replay_timestamp() gives seconds behind; on Aurora/RDS use the AuroraReplicaLag / ReplicaLag CloudWatch metric (milliseconds). Set thresholds: warn at 1s, page at 5s, and evict the replica from the read pool at 10s so a badly-lagging replica stops serving stale reads instead of quietly poisoning them.
  2. Classify every read by its staleness budget. Write a two-column contract: reporting / analytics → 30s stale OK → replica; list & search → 5s stale OK → replica; balance & post-write reads → 0s → read-your-writes. The budget, not the endpoint's name, decides the route.
  3. Give the zero-budget reads read-your-writes. The cheap version: after any write, set a sticky-primary flag keyed on user_id for a 5s window (a little above measured p99 lag) so that user's next reads go to the primary. The precise version: capture the write's log position — pg_current_wal_lsn() (Postgres) or the returned GTID (MySQL) — as a consistency token, and only let a replica serve the read if its applied position has caught up (pg_last_wal_replay_lsn() >= token, or SELECT WAIT_FOR_EXECUTED_GTID_SET(gtid, 0.2)); otherwise wait up to ~200ms or fall back to the primary.
  4. Route on live lag, not static config. Put a lag-aware proxy in front — Aurora reader endpoint, RDS Proxy, ProxySQL/PgBouncer query rules — that health-checks replica lag every few seconds and pulls a replica out of rotation the moment it crosses the eviction threshold, so routing tracks reality.
flowchart TD
    A["Read request"] --> B{"Staleness budget?"}
    B -->|"0s (read-your-writes)"| C{"Wrote in last 5s<br/>OR token not applied?"}
    C -->|"yes"| D["Primary (fresh)"]
    C -->|"no"| E["Replica"]
    B -->|"5-30s OK"| F{"Replica lag < threshold?"}
    F -->|"lag < 10s"| E
    F -->|"lag >= 10s (evicted)"| D
    E --> G["Scaled reads, primary protected"]
    D --> G

When to use it — and when not

Reach for it whenever you offload reads to async replicas to protect a primary (Drill 79) or to scale read throughput, and some of those reads must not go backward in time. The cost is real: a sticky-primary window and consistency-token checks claw read traffic back onto the primary — the very load you were shedding — so if most reads need read-your-writes, replicas buy you little and you should scale the primary or shard instead. Skip the machinery entirely for reads that are already lag-tolerant (dashboards, analytics, search over yesterday's data) — just route them to the replica with a generous staleness budget. And don't reach for async replicas at all when you need strong, always-fresh reads globally: that calls for synchronous replication or a consensus store (Spanner-style), where you pay latency for freshness rather than managing lag.

Signals it's working

Seen in the wild

See it

The ChainScene carries the routing decision — measure lag → classify by staleness budget → read-your-writes reads pin to the primary, lag-tolerant reads hit the replica → stale reads = 0; the CardsScene shows the load-bearing numbers (warn 1s / page 5s / evict 10s, a 5s sticky-primary window, a ~200ms token wait).

Named concepts: read-your-writes consistency, replica lag monitoring, lag-aware routing, bounded staleness