Read replicas: when routing beats caching
Caching pushes read load off the database until invalidation storms or stale data break trust. Read replicas with explicit query routing trade operational complexity for predictable latency — and a path that survives when the cache cannot.
Read traffic doubled. The team added Redis. Hit rates looked fine — 94%. Then a bulk admin update invalidated half the keyspace and Postgres absorbed the thundering herd anyway. Dashboard p99 spiked to four seconds. Cache-aside had hidden the read load until hiding failed. Postgres read replica routing addresses a different problem: distribute read queries to standby nodes with known replication lag tradeoffs — instead of hoping invalidation logic stays correct under every write pattern.
Caching and replicas are complements, not substitutes. Caching wins on hot keys with tolerable staleness. Replicas win on read-heavy analytical queries, unpredictable access patterns, and workloads where cache invalidation is harder than replication lag management.
When caching stops being enough
| Symptom | Likely cause | Replica vs cache |
|---|---|---|
| Invalidation storms after bulk writes | Cache key explosion | Replicas absorb read spike directly |
| Stale reads break user trust | TTL too long or missed invalidation | Replicas: bounded lag, often sub-second |
| Memory cost exceeds DB read cost | Large working set | Replicas cheaper per GB scanned |
| Complex query patterns | Hard to key cache entries | Replicas run SQL; cache needs bespoke keys |
| Cold cache after deploy | All misses at once | Replicas always warm for SQL path |
Caching hides load. It does not remove it — it delays and concentrates it at miss time. Replicas spread load continuously at cost of replication lag and operational routing complexity.
A cache with wrong invalidation is a stale read machine with good hit rate metrics.
Read replica routing architecture
Primary handles writes and optionally strongly consistent reads.
Replicas handle read-only queries routed explicitly — not accidentally.
Routing layers:
-
Application-level. Code passes
read_consistency: strong | eventualflag; connection pool selects primary or replica datasource. -
ORM / middleware. Read/write splitter intercepts
SELECTvsINSERT/UPDATE/DELETE. -
Proxy. PgBouncer, RDS Proxy, or custom router — centralizes routing; risk is opaque magic if teams do not understand rules.
-
CQRS read models. Dedicated projection tables on replica or separate read store — see CQRS read model for when projections replace ad hoc replica routing.
Explicit routing rules beat implicit "all SELECTs go to replica":
| Query type | Route | Reason |
|---|---|---|
| User session auth check | Primary | Must see latest credential state |
| Post-checkout order status | Primary | User just paid — lag unacceptable |
| Analytics dashboard | Replica | Seconds of lag OK |
| Search/list pagination | Replica | High volume, eventual OK |
| Reporting exports | Replica | Long scans harm primary |
| Read-after-write in same request | Primary | Classic replication lag bug |
Replication lag is the replica's cache TTL
Replicas are eventually consistent. Lag varies:
- Normal: milliseconds to low seconds
- Heavy write load: seconds
- Replica maintenance, network blip: minutes possible
Mitigations:
Lag monitoring. Alert when replica lag exceeds SLO — same discipline as SLIs without SRE team.
Critical read pinning. After write, route subsequent reads in same session to primary for N seconds or until replication catch-up confirmed.
RPO/RTO awareness. Replica promotion for failover is different from read scaling — know which replicas are candidates for promotion vs read-only analytics.
Avoid write-heavy "read" queries. Long reports that lock rows on replica still hurt — replicas are not free compute, they are copy of primary write path.
Combining cache and replicas
Healthy stack uses both:
Write → Primary
Read (hot, stale-OK) → Cache → on miss → Replica
Read (must be fresh) → Primary
Read (heavy scan) → Replica (bypass cache — don't cache 10MB result sets)
Cache the expensive computed results from replica reads — not every row lookup. Invalidation triggers on write still required, but replica absorbs cache miss SQL load.
Do not cache what replicas handle cheaply at your scale. Do not route to replicas what requires millisecond-fresh reads.
Connection pool sizing splits across primary and replicas — connection pooling in serverless applies to both; starving replica pools causes app-side timeouts that look like database failures.
Operational checklist for replica routing
- Document per-endpoint consistency requirement — strong vs eventual.
- Integration tests that assert read-after-write paths hit primary.
- Load test replica path independently — replica saturation is separate incident.
- Query review: N+1 read patterns hurt replicas same as primary — fix queries before adding infra.
- Index replicas same as primary — replicas execute same plans; missing indexes hurt twice.
How should teams choose replica routing over more caching?
These decisions clarify when to invest in routing complexity.
When is replication lag acceptable?
When product tolerates seconds-old data on read — dashboards, listings, recommendations. Not when user just mutated entity and expects immediate reflection — pin to primary.
How many replicas are enough?
Scale until read p99 on routed paths meets SLO under peak. One replica is single point of failure for read path — plan for replica loss failing over reads to primary with capacity headroom or second replica.
Should ORM auto-route all SELECTs to replica?
Dangerous default. Auto-routing without per-query consistency flags causes subtle bugs — checkout status from replica, permission checks from replica. Explicit beats magic.
A common argument runs the other way
The opposing view holds that replicas duplicate cost and complexity — that better caching and query optimization eliminate read load on primary.
Query optimization is mandatory regardless. Caching helps hot paths. Replicas address long-tail read volume and unpredictable scans that resist cache keying. At sufficient read/write ratio, replica cost is predictable; cache miss storms are not.
Serverless Postgres offerings often bundle read replicas — routing discipline matters even when infra is managed.
Key takeaways
- Caching hides read load until invalidation fails; replicas spread load with bounded replication lag.
- Explicit per-query routing — strong reads to primary, eventual reads to replica.
- Read-after-write in same session must pin to primary or wait for catch-up.
- Combine cache (hot results) with replicas (SQL execution) — not either-or.
- Monitor replica lag as SLI; alert before users see stale data.
- Index and optimize queries before scaling replicas — bad SQL scales badly everywhere.
Conclusion
Read replica routing is honesty about consistency: some reads tolerate lag, some do not. Caching without that honesty produces hit rates that lie about system health. Teams that document routing rules per endpoint sleep through bulk updates; teams that route all SELECTs to one replica wonder why checkout shows paid orders as pending.
Start with endpoint inventory: which reads require strong consistency? Everything else is replica candidate — then measure lag, then add cache on top where economics justify.