Most web applications read far more than they write — think 90% reads, 10% writes. When a single database starts straining under read load, read replicas are the most common scaling tool. This guide explains how they work, the crucial consistency caveat (replication lag), and when they actually help.
The core idea
A read replica is a copy of your primary database that stays continuously synchronized with it. The split of duties:
- Primary (writer): handles all writes (
INSERT,UPDATE,DELETE) and can also serve reads. - Replica(s) (readers): receive a stream of changes from the primary and serve read-only queries.
You route writes to the primary and spread reads across replicas. Because reads dominate, this offloads the bulk of traffic from the primary.
writes
App ───────────────▶ Primary
│ │ replication stream
│ reads ▼
└──────────────▶ Replica 1, Replica 2, ...How replication works
The primary records every change in a log (PostgreSQL's WAL — Write-Ahead Log; MySQL's binlog). Replicas connect, stream that log, and replay the changes to stay in sync. Two flavors:
| Mode | Behavior | Trade-off |
|---|---|---|
| Asynchronous | Primary commits without waiting for replicas | Fast writes; replicas can lag |
| Synchronous | Primary waits for replica acknowledgment | Stronger consistency; slower writes |
Most read-scaling setups use asynchronous replication because it keeps writes fast. That choice has a direct consequence you must design around: replication lag.
The big caveat: replication lag
With async replication, there's a brief window where a write has committed on the primary but hasn't yet reached the replicas. During that window, a read from a replica returns stale data.
Concretely — the dreaded bug:
- 1User updates their profile name (write → primary).
- 2The page reloads and reads the profile (read → replica).
- 3The replica hasn't caught up yet → user sees their old name.
- 4User is confused; "my change didn't save!"
This is the read-your-own-writes problem, and it's the number-one gotcha with replicas. Lag is usually milliseconds, but under load or large transactions it can spike.
Strategies for the lag problem
You don't avoid lag; you design around it:
- Read-after-write from the primary. For a short window after a user writes, route that user's reads to the primary. Solves the profile-name bug directly.
- Sticky reads / monotonic reads. Keep a user pinned to one replica so they don't bounce between replicas at different lag levels.
- Tolerate staleness where it's fine. A public feed, a product catalog, analytics dashboards — slightly stale is perfectly acceptable. Route these to replicas freely.
- Read critical data from the primary. Anything where staleness is dangerous (account balance shown before a transfer) reads from the primary.
The meta-strategy: classify each read by its staleness tolerance and route accordingly.
When read replicas help
Replicas are the right tool when:
- Your workload is read-heavy (the common case).
- The primary is CPU/IO-bound on reads, not writes.
- You can tolerate eventual consistency for most reads.
- You want read availability if the primary has issues (a replica can often be promoted).
When read replicas DON'T help
Be honest about the limits — replicas are not a universal scaling button:
- Write-bound workloads. Every write still goes to the single primary. Replicas don't add write capacity — they can even *add* write overhead (the primary must ship the replication stream). If writes are your bottleneck, you need sharding or a different architecture, not replicas.
- Strong-consistency-everywhere requirements. If every read must be perfectly current, async replicas fight you.
- Tiny databases. If your DB isn't actually saturated, replicas add complexity for no gain. Measure first.
Replicas vs other scaling approaches
| Approach | Scales | Complexity | Notes |
|---|---|---|---|
| Vertical (bigger box) | Reads + writes | Low | Hits a ceiling; single point of failure |
| Read replicas | Reads | Medium | Lag caveat; doesn't help writes |
| Caching (Redis) | Reads | Medium | Cache invalidation is hard |
| Sharding | Writes + reads | High | Complex; for genuine write scale |
A pragmatic progression: scale vertically first (simplest), add caching for hot reads, add replicas for broad read load, and only shard when writes truly exceed one primary.
Promotion and failover
A useful bonus: replicas double as standby copies. If the primary fails, a replica can be promoted to primary, providing high availability — not just read scaling. Note that with async replication, a promoted replica may be missing the last few unreplicated writes, which is the durability trade-off of async.
How this maps to PandaStack
PandaStack provides managed databases — PostgreSQL (14.x/16.x), MySQL (5.7/8.x), MongoDB, Redis — via KubeBlocks on GKE, with scheduled and manual backups. The right default for most apps is a single managed primary; replicas become valuable as your read load grows and you can classify which reads tolerate slight staleness. Pair replicas with a Redis cache for hot reads, and you can take a single primary a very long way.
References
- [PostgreSQL: High Availability and Replication](https://www.postgresql.org/docs/current/high-availability.html)
- [PostgreSQL WAL (Write-Ahead Logging)](https://www.postgresql.org/docs/current/wal-intro.html)
- [MySQL replication](https://dev.mysql.com/doc/refman/8.0/en/replication.html)
- [Read-your-writes consistency](https://jepsen.io/consistency/models/read-your-writes)
- [KubeBlocks](https://kubeblocks.io/)
---
Managed databases on PandaStack come with backups and auto-wiring (DATABASE_URL injected into your app), so you can start with a solid primary and grow your read strategy as needed. The free tier includes a database — try it at [dashboard.pandastack.io](https://dashboard.pandastack.io).