Quick answer
A read replica is a copy of a database that receives changes from a primary and serves read-only traffic. Replicas can increase read capacity, isolate analytical queries, and place data closer to users. They do not increase primary write capacity, and asynchronous replicas can return stale data because changes take time to ship and apply.
The hard part is not creating the replica. It is deciding which reads may be stale, preserving read-your-writes where users expect it, measuring replica lag, and failing safely when a replica falls behind or is promoted.
How replication works
In a common PostgreSQL physical replication setup, the primary records changes in the write-ahead log (WAL). A standby receives WAL records and replays them against a copy of the data files. A hot standby may accept read-only queries while replay continues.
client write -> primary commits -> WAL shipped -> replica receives -> replica replays
^ network ^ apply queue
Lag can accumulate while sending, receiving, flushing, or replaying changes. A slow query on the replica, insufficient CPU or I/O, a network problem, a burst of writes, replay conflicts, or a missing WAL segment can all widen the gap.
Logical replication sends changes at a logical table/row level and supports different topologies, but it also has apply capacity, schema, and conflict considerations. Managed services hide some mechanics, not the correctness trade-off.
Asynchronous versus synchronous replication
With asynchronous replication, a primary may acknowledge a commit before a replica receives it. This keeps write latency and availability less dependent on the replica, but a reader can see old state. If the primary is permanently lost, acknowledged changes not yet replicated may also be absent after failover.
Synchronous replication makes commit acknowledgment wait for configured standby feedback. It can improve durability guarantees for acknowledged writes, but adds a network or flush round trip. If the required standby is unavailable, commits may block or the system must change policy.
Synchronous commit does not mean every read endpoint is automatically current. The exact setting may wait for receipt, disk flush, or apply, and other asynchronous replicas can still lag. Verify the product’s documented acknowledgment point.
Replica lag is a correctness signal
Time-based lag is easy to display but can mislead. A quiet primary may not emit a fresh timestamp, while a busy replica can be only seconds behind but missing thousands of important transactions. Track both time and log position/bytes when the engine exposes them.
For PostgreSQL, teams commonly compare primary WAL locations with replica receive and replay locations, alongside replay timestamp. Managed services may publish a ReplicaLag metric. Alerting needs workload context and a defined stale-read budget.
Useful service-level objectives include:
- 99.9% of ordinary replica reads are less than two seconds behind.
- Security and billing reads never route to an asynchronous replica.
- A replica is removed from routing when apply lag exceeds a threshold.
- Failover recovery is practiced with a stated maximum data-loss objective.
Lag is not only an infrastructure dashboard. If users observe stale state, attach route choice and consistency requirements to traces so operators can explain the response.
Read-after-write failures
A user changes an email address on the primary, then the next page loads from a replica that has not applied the write:
POST /profile -> primary -> 200 with version 8421
GET /profile -> replica at version 8418 -> old email
The user may retry, creating duplicate work or believing the write failed. More dangerous examples include a permission grant followed by a check, an order creation followed by lookup, or a password reset token immediately queried from a lagging copy.
Patterns for read-your-writes include:
- Return the committed representation and render it without an immediate reread.
- Pin a session or entity to the primary for a short time after a write.
- Carry a commit/log position token and use a replica only after it catches up.
- Route correctness-sensitive endpoints permanently to the primary.
- Model a visible pending state when asynchronous propagation is part of the product.
Time-based pinning is simple but approximate. A causal token is more precise but requires engine and routing support. Never sleep for an arbitrary interval and assume replication completed.
Which reads belong on replicas
Good candidates are browse pages, search-adjacent views, reports, exports, historical dashboards, recommendation inputs, and expensive read-only workloads with explicit freshness expectations.
Poor candidates include authorization decisions, uniqueness checks, inventory confirmation, payment state, idempotency records, job ownership, and a read that must immediately observe a just-committed write.
The query shape matters too. A reporting query can saturate replica I/O and delay WAL replay, making every other result staler. Use workload isolation, statement timeouts, indexes, and possibly dedicated analytical systems rather than treating one replica as unlimited capacity.
Data residency can also constrain routing. A cross-region replica may improve latency but create compliance, encryption, or deletion-propagation obligations. Record which datasets may be replicated to each location.
Routing architecture
A common application has separate pools:
commands and critical reads -> writer pool -> primary
stale-tolerant queries -> reader pool -> healthy replicas
Keep the choice explicit in repository or service interfaces. A magical SQL parser that sends every SELECT to a replica fails when a read participates in a transaction, follows a write, uses a locking clause, or calls a function with side effects.
Transactions should stay on one connection and one role. Once a transaction begins on the writer, all its statements—including reads—should use that writer connection unless the database provides a deliberately designed distributed transaction model.
Spring routing example
Spring’s AbstractRoutingDataSource can select a target before a connection is obtained:
public final class RoleRoutingDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<Boolean> READ_ONLY = new ThreadLocal<>();
public static void useReplica(boolean value) {
READ_ONLY.set(value);
}
public static void clear() {
READ_ONLY.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return Boolean.TRUE.equals(READ_ONLY.get()) ? "reader" : "writer";
}
}
Treat this as infrastructure, not a full policy. A request filter or transaction interceptor must set and always clear context. @Transactional(readOnly = true) is commonly used as a routing hint, but read-only is not a correctness proof. A service method that needs fresh state should declare writer routing even if it executes only SELECT.
A safer domain-facing API can make intent visible:
public Account loadAccount(long id, Consistency consistency) {
return consistency == Consistency.STRONG
? writerRepository.findById(id)
: readerRepository.findById(id);
}
Pools need separate metrics. If one replica is unhealthy, remove only that target rather than silently sending every stale-tolerant query to an already overloaded primary.
Node.js routing example
Create distinct pg.Pool instances and choose at a service boundary:
import pg from "pg";
const writer = new pg.Pool({ connectionString: process.env.DATABASE_WRITER_URL });
const reader = new pg.Pool({ connectionString: process.env.DATABASE_READER_URL });
export async function getOrder(orderId, { fresh = false } = {}) {
const pool = fresh ? writer : reader;
const result = await pool.query(
"select id, status, total_cents, updated_at from orders where id = $1",
[orderId]
);
return result.rows[0] ?? null;
}
export async function createOrder(input) {
const result = await writer.query(
`insert into orders(customer_id, total_cents)
values ($1, $2)
returning id, status, total_cents, updated_at`,
[input.customerId, input.totalCents]
);
return result.rows[0];
}
The create response avoids an immediate replica reread. For a multi-statement transaction, acquire one client from writer, run every statement on it, and release it in finally.
Failover is not read scaling
A high-availability standby and a read-scaling replica may be different resources. A managed Multi-AZ standby can be reserved for failover and not serve queries. A read replica can itself need redundancy.
Promotion creates a new writable history. The application must update endpoints, drain or fence the old primary, and prevent split-brain writes. DNS caches, connection pools, and long-lived sessions can keep using an obsolete address after control-plane failover.
Define recovery point objective (possible data loss) and recovery time objective (time to restore service) separately. Test promotion, connection refresh, lag validation, and old-primary fencing. A successful console button is not a completed application failover.
Replicas are not backups
Replication promptly copies accidental deletes, corrupt updates, and harmful schema changes. A replica improves availability and read capacity; a backup provides an independent recovery point. Use versioned backups, retention, point-in-time recovery, and restore drills.
Delayed replicas can add a recovery window, but they consume retained logs and still need operational protection. They complement rather than replace a backup strategy.
Common mistakes
The first mistake is routing all SELECT statements to replicas. Some reads are part of correctness decisions.
The second is monitoring only replica health, not lag. A process can be “running” while hours behind.
The third is hiding stale state from product teams. Freshness must be a documented endpoint contract.
The fourth is adding replicas without sizing apply capacity. More read traffic and long queries can slow replay.
The fifth is assuming promotion is lossless because the replica looks healthy. Asynchronous lag defines potential loss.
Practical checklist
- Classify every read as strong, session-consistent, or stale-tolerant.
- Maintain explicit writer and reader connection pools.
- Keep each transaction on one writer connection.
- Return committed representations after writes when possible.
- Implement read-your-writes with pinning or a version token.
- Track receive/apply positions, time lag, and replay errors.
- Remove lagging replicas from routing using a tested threshold.
- Set query timeouts and isolate heavy reporting work.
- Budget database connections across primary and replicas.
- Document synchronous versus asynchronous acknowledgment semantics.
- Test promotion, DNS refresh, pool recycling, and old-primary fencing.
- Maintain independent backups and verified restores.
Frequently asked questions
Do read replicas make writes faster?
Not directly. They move eligible reads away from the primary. Write scaling usually requires batching, schema/query changes, partitioning, or a different architecture.
Can a synchronous replica return stale data?
Yes, depending on whether commit waits for receipt, flush, or replay and which replica serves the read. Check the configured guarantee.
How long should primary pinning last?
Base it on measured lag and business risk, or use a commit-position token. A fixed delay is a pragmatic fallback, not a proof.
Should an API expose consistency options?
Sometimes. Internal services often use separate methods or repositories. Public APIs can expose freshness only when clients understand the latency and availability trade-off.
Can I use a replica for backups?
You may take a backup from a replica to reduce primary load if the engine supports a consistent procedure, but replication itself is not a backup.
Sources and further reading
- PostgreSQL, Warm Standby and Streaming Replication.
- Amazon RDS, Working with Read Replicas for PostgreSQL.
- Spring Framework, AbstractRoutingDataSource.
- node-postgres, Pool.
Related reading
Read Eventual Consistency Explained for stale-state product design, Database Connection Pooling Explained for capacity planning, and Database Backup and Restore Explained for independent recovery. CAP Theorem Explained frames the consistency and availability decision during network loss.