On this page
What Is a Read Replica?
A read replica is a copy of your database that follows the primary. It scales reads, and it introduces replication lag — which is a correctness problem, not just a delay.
Quick answer
- A read replica is a second database that receives the primary’s changes continuously and can serve read queries.
- Replication is usually asynchronous, so the replica is slightly behind — that delay is called replication lag and it is the thing to design around.
- Reads that must reflect the user’s own recent write belong on the primary, or need read-your-writes handling, because the replica may not have caught up.
- Replicas scale read throughput and can provide a standby for failover; they do not help with write volume at all.
- The common bug is sending a just-updated record to a replica and showing the user their own change as missing.
What is a read replica?
It is a copy of the primary database maintained by continuous replication. The primary applies writes and streams them — typically as a log of changes — to each replica, which applies the same changes in order. Reads sent to the replica see the same data a short time later. Because every replica applies the same write stream, the replica cannot accept most writes itself without diverging, which is why the topology is one writer and many readers.
Replication is normally asynchronous: the primary commits a write and acknowledges the client without waiting for the replica to apply it. That makes writes fast and means the replica lags, usually by milliseconds and occasionally by much more under load. Some systems offer synchronous replication, which waits for one replica — improving durability and consistency at the cost of write latency and availability if that replica is unreachable.
What it fixes and what it does not
It fixes read scaling. A read-heavy application — most applications, where reads outnumber writes many times over — can send the bulk of its queries to replicas and keep the primary for writes and for consistency-sensitive reads. Adding replicas multiplies read capacity roughly linearly, up to the point where the primary’s replication stream is the bottleneck.
It does nothing for write volume. Every write goes to the primary regardless of how many replicas exist, so a write-bound workload gains nothing. It also does not automatically fix slow reads: a badly indexed query on a replica is still slow, just less disruptive. Replicas distribute load; they do not remove the need for the index or the schema fix that the query needs.
Replication lag is a correctness constraint
The central hazard is read-your-own-writes. A user submits a form, the write goes to the primary, the page then reads from a replica that has not caught up, and the user is shown the old state. To them the application lost their change, and they submit again. The same problem appears in a checkout flow, a settings screen, and any confirmation page — anywhere a read immediately follows a write.
Three remedies, in increasing order of complexity. Route the read after a write to the primary, which is simple and correct but caps that path’s read scaling. Track the write’s log position and require the replica to have caught up to it before serving the read, which the client can express as a wait on a timestamp or a position. Or make the read idempotent and tolerate staleness, which is acceptable for a feed and not for a confirmation. The wrong answer is to ignore the lag and hope it is small.
# Route by need: a write, then a read that must see it.
def update_profile(primary, user_id, name):
# Capture the write position so a read can wait for it.
with primary.cursor() as cur:
cur.execute('UPDATE users SET name = %s WHERE id = %s', (name, user_id))
cur.execute('SELECT pg_current_wal_lsn()')
return cur.fetchone()[0]
def read_after_write(replica, user_id, wait_for_lsn=None):
if wait_for_lsn is not None:
# Block until the replica has applied at least this position.
with replica.cursor() as cur:
cur.execute(
"SELECT pg_last_wal_replay_lsn() >= %s::pg_lsn",
(wait_for_lsn,),
)
# A short bounded wait, then fall back to the primary.
if not cur.fetchone()[0]:
return None # caller should read from the primary instead
with replica.cursor() as cur:
cur.execute('SELECT name FROM users WHERE id = %s', (user_id,))
return cur.fetchone()[0]Replicas as failover, and their own costs
A replica can be promoted to become the new primary when the old one fails, which is the basis of most high-availability setups. Asynchronous replication means promoting loses the writes the replica had not yet applied, so failover is fast but potentially lossy; synchronous replication narrows that window at the cost of write latency. Understanding which guarantee you have is part of choosing the configuration, not an implementation detail.
The running costs are operational. Every replica consumes a full copy of the storage, so backup and restore time grow with the replica count. Long-running analytical queries on a replica can slow its replication, pushing lag up and widening the window in which the application can read stale data. And once reads are split across replicas by a connection pool, monitoring has to be per replica, because one lagging replica produces intermittent staleness that is invisible in an aggregate.
Where this bites vibecoders
Generated code typically writes to one connection and reads from the same one, which is correct and does not scale. When someone adds a replica for read capacity, the same code silently reads stale data after every write, and the bug appears as a confirmation page showing old values. The fix belongs in the data-access layer where the decision is made, not in each handler, because the handler is the wrong place to remember which read needs the primary.
Where AI coding assistants get this wrong
- Reading a just-updated record from a replica and showing the user stale data.
- Assuming replication is synchronous and the replica is always current.
- Sending large analytical queries to a replica and slowing its replication.
- Adding replicas to fix a write bottleneck, which they cannot help with.
- Ignoring lag in failover planning, so recent writes are lost on promotion.
- Monitoring only the primary and never tracking per-replica lag.
Checklist
- Confirm the bottleneck is reads, not writes.
- Identify the reads that immediately follow a write and route them to the primary.
- Implement a bounded wait against the replica’s applied position where needed.
- Monitor lag per replica and alert when it exceeds your staleness bound.
- Keep expensive analytics off replicas that serve user traffic.
- Decide and document what failover loses under asynchronous replication.
FAQ
Can a read replica replace a backup?
No. A replica mirrors the primary’s changes including mistakes and corruption, so a bad deletion is replicated within moments. It protects against hardware failure and provides read capacity, not against human error. Backups are a separate, versioned copy that you can restore to a point in time, and you still need one.
How much lag is acceptable?
That is a product decision, expressed as the staleness a user can tolerate, not a technical default. For a feed, seconds may be fine; for a confirmation screen, none is acceptable and the read must go to the primary. Decide it per read path, because a single global answer will either be too strict to scale or too loose to be correct.
Why does my replica fall behind under load?
Usually because it is doing other work — long analytical queries competing with the replication apply process — or because a single-threaded apply cannot keep up with a parallel write load. Look at what else runs on the replica, then at the apply throughput. Moving analytics off it, or adding replicas so each serves less, are the usual fixes.
Should reads always go to a replica?
No. Reads that must reflect the current user’s write, reads inside a transaction that also writes, and reads used to make a decision should go to the primary. Routing everything indiscriminately is the configuration that produces the stale-confirmation bug, and the distinction is easier to express once, in the data layer, than in every handler.
Related topics
- What Is Eventual Consistency (and Why Do My Reads Return Stale Data)?
- Eventual Consistency vs Strong Consistency: What’s the Difference (and When Does It Matter)?
- What Is Sharding?
- What Is a Materialized View?
- What Is a Connection Pool?
- Databases for AI Builders: Why Your AI-Generated App Gets Slow and Loses Data