Read replicas
Backend Fundamentals · Databases · 8 min read
1. What problem does this solve?
- Problem: A single database instance becomes a bottleneck when many read requests compete with writes. Reads can block writes, and a single point of failure can bring the whole service down.
- Why it matters:
- Throughput – Heavy read traffic can saturate CPU, I/O, or network of the primary node.
- Availability – If the primary crashes, the entire application stops serving data.
- Latency – Reads from a distant primary can be slow; a closer replica can reduce round‑trip time.
- What happens if we ignore it:
- Write latency increases because the primary must lock rows for every read.
- The application may become unresponsive under load.
- A single failure can cause downtime.
- Real‑world usage:
- E‑commerce sites use read replicas to serve product catalogs while writes update inventory.
- Social networks replicate user profiles to serve feeds.
- SaaS dashboards read analytics from replicas while writes happen in the background.
2. Mental model
Analogy
Think of a library that has one master copy of a book (the primary). Every time someone wants to read the book, they must go to the master copy. If many people come at once, the master copy becomes a bottleneck. To solve this, the library creates duplicate copies (read replicas) that people can read from freely. The master still updates the original, and the duplicates are periodically refreshed.
Engineering model
- Primary (master): Handles all writes and is the source of truth.
- Replica(s): Read‑only copies that receive changes from the primary via replication streams.
- Replication lag: The time between a write on the primary and its appearance on a replica.
- Consistency model: Usually eventual – replicas may be slightly stale.
3. Deep explanation
| Topic | Explanation |
|---|---|
| Internals | Replication can be synchronous (writes wait for replicas) or asynchronous (writes return immediately). Most systems use asynchronous for performance. The primary logs changes (write‑ahead log, WAL) and streams them to replicas over a network connection. Replicas replay the log to stay in sync. |
| Important terminology | WAL (Write‑Ahead Log), replication slot, logical replication, physical replication, replication lag, failover, promotion, read‑only mode, hot standby. |
| Trade‑offs | Latency vs. consistency: Asynchronous replication gives low write latency but stale reads. Synchronous gives strong consistency but higher latency. More replicas increase read capacity but add network traffic and storage. |
| Failure modes | Replica becomes out of sync (lag too high), replication connection drops, primary fails, network partition. |
| Limitations | Replicas cannot accept writes (unless using multi‑master or sharding). They cannot enforce foreign key constraints that depend on writes. |
| Where it fits | In a typical microservice architecture, a service may have a primary database instance and one or more read replicas. The application layer routes read queries to replicas and writes to the primary. |
| Interaction with related concepts | Connection pooling (different pools for read/write), caching (Redis), CQRS (Command Query Responsibility Segregation), event sourcing, load balancers, failover orchestration (Patroni, PgBouncer, etc.). |
4. How it works
- Primary writes: Application sends a write to the primary. The primary writes to its WAL and applies the change.
- WAL streaming: The primary streams WAL entries to each replica over a persistent TCP connection.
- Replica replay: Each replica receives WAL entries, replays them, and updates its local copy.
- Read routing: Application routes read queries to replicas (often via a load balancer or a routing library).
- Consistency check: If a read must be strongly consistent, the application can read from the primary or use a read‑your‑writes pattern (e.g., read from primary after a write).
- Failover: If the primary fails, a replica can be promoted to primary (often automatically by an orchestrator).
sequenceDiagram
participant App as Application
participant Primary as Primary DB
participant Rep1 as Replica 1
participant Rep2 as Replica 2
App->>Primary: WRITE (INSERT)
Primary->>Primary: WAL entry
Primary->>Rep1: WAL stream
Primary->>Rep2: WAL stream
Rep1->>Rep1: Replay WAL
Rep2->>Rep2: Replay WAL
App->>Rep1: READ (SELECT)
App->>Rep2: READ (SELECT)
5. Practical implementation
Node.js + PostgreSQL example
// db.ts
import { Pool } from 'pg';
// Primary pool (writes)
export const primaryPool = new Pool({
connectionString: process.env.DATABASE_URL, // e.g., postgres://user:pass@primary:5432/app
application_name: 'app-primary',
});
// Replica pool (reads)
export const replicaPool = new Pool({
connectionString: process.env.DATABASE_REPLICA_URL, // e.g., postgres://user:pass@replica:5432/app
application_name: 'app-replica',
});
// service.ts
import { primaryPool, replicaPool } from './db';
export async function createUser(name: string, email: string) {
const client = await primaryPool.connect();
try {
await client.query('BEGIN');
const res = await client.query(
'INSERT INTO users(name, email) VALUES($1, $2) RETURNING id',
[name, email]
);
await client.query('COMMIT');
return res.rows[0].id;
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
}
export async function getUser(id: number) {
// Reads go to replica
const res = await replicaPool.query('SELECT * FROM users WHERE id = $1', [id]);
return res.rows[0];
}
Key points
- Separate pools keep connections isolated; writes never hit replicas.
application_namehelps DB admins identify traffic sources.- In production, use a load balancer (e.g., PgBouncer) to distribute read traffic across many replicas.
Docker Compose for a simple replica setup
# docker-compose.yml
version: '3.9'
services:
db-primary:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db-primary-data:/var/lib/postgresql/data
ports:
- "5432:5432"
db-replica:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db-replica-data:/var/lib/postgresql/data
depends_on:
- db-primary
command: >
postgres -c wal_level=replica
-c max_wal_senders=5
-c hot_standby=on
ports:
- "5433:5432"
volumes:
db-primary-data:
db-replica-data:
After starting, configure the replica to connect to the primary:
-- On db-replica
CREATE PUBLICATION app_publication FOR ALL TABLES;
CREATE SUBSCRIPTION app_subscription
CONNECTION 'host=db-primary port=5432 user=postgres password=secret dbname=app'
PUBLICATION app_publication;
6. Production considerations
| Concern | How to address |
|---|---|
| Scalability | Add more replicas behind a load balancer; use connection pooling. |
| Reliability | Monitor replication lag; set alerts for lag > threshold. Use failover tools (Patroni, PgBouncer). |
| Security | Encrypt replication traffic (SSL/TLS). Restrict replica access to specific IPs. |
| Observability | Expose metrics: pg_stat_replication, pg_stat_wal_receiver. Use Prometheus exporters. |
| Performance | Tune max_wal_senders, wal_keep_segments, and wal_compression. Use hot_standby_feedback to avoid wraparound. |
| Error handling | Detect lost replication connections; automatically restart or promote. |
| Resource usage | Replicas consume disk and I/O; monitor disk space and IOPS. |
| Operational concerns | Backup strategy: take base backups from primary; replicas can be used for read‑only backups. |
| Failure recovery | Automated promotion scripts; keep a “standby” node ready. |
| Deployment | Use immutable containers; version‑controlled replication configs. |
7. Common mistakes
| Mistake | Why it’s problematic |
|---|---|
| Using replicas for writes | Replicas are read‑only; writes will fail or be ignored. |
| Ignoring replication lag | Assuming replicas are always fresh leads to stale reads and bugs. |
| Routing all traffic to replicas | Some queries (e.g., those that need fresh data) must hit the primary. |
Not monitoring pg_stat_replication | Lag can silently grow, causing data inconsistency. |
| Hard‑coding replica hosts | Fails when scaling or when replicas change IPs. Use service discovery or a load balancer. |
| Over‑optimizing for latency | Pulling too many replicas can increase network traffic and cost. |
| Failover without promotion | If the primary dies, replicas stay read‑only; the app cannot write. |
8. Senior engineer perspective
What changes at scale?
- Architecture decisions
- Decide between physical vs. logical replication.
- Choose a failover orchestrator (Patroni, Stolon, etc.).
- Integrate with CI/CD to roll out replication config changes safely.
- Trade‑offs
- Synchronous replication gives strong consistency but hurts write latency.
- Asynchronous replication is cheaper but requires careful lag monitoring.
- Multi‑region replicas improve latency for global users but increase replication traffic.
- Bottlenecks
- Network bandwidth between primary and replicas.
- Disk I/O on the primary when many replicas are connected.
- CPU on replicas when replaying large WAL streams.
- Failure scenarios
- Network partition: replicas may become isolated; need split‑brain detection.
- Primary crash: automated promotion must be fast and safe.
- Monitoring
- Metrics: replication lag, WAL size,
pg_stat_replicationstatus. - Alerts: lag > 5 s, replication connection lost, disk space low.
- Metrics: replication lag, WAL size,
- Cost/performance
- Replicas cost compute and storage; balance read capacity vs. budget.
- Use spot instances or cheaper regions for read replicas if latency tolerable.
- When NOT to use
- If the workload is write‑heavy and reads are negligible.
- If strict read consistency is required (e.g., banking).
- If the database vendor does not support efficient replication (e.g., some NoSQL).
9. Interview questions
1BeginnerWhat is a read replica and why would you use one?
2BeginnerHow does PostgreSQL achieve replication?
3SeniorExplain the difference between physical and logical replication in PostgreSQL, including use cases.
4SeniorDescribe how you would monitor and alert on replication lag in a production system.
pg_stat_replication for pg_last_wal_receive_lsn vs. pg_last_wal_replay_lsn, expose metrics to Prometheus, set alerts when lag exceeds a threshold, and correlate with application latency.5Scenario/DebuggingYour application suddenly starts returning stale data from a read replica. What steps would you take to diagnose and fix the issue?
- Check replication status (
pg_stat_replication) for lag. - Verify network connectivity between primary and replica.
- Look at WAL receiver logs for errors.
- Confirm that the replica is still connected and not in recovery mode.
- If lag is high, investigate load on primary or network bottlenecks.
- Consider temporarily routing critical reads to primary until resolved.
10. Hands‑on task
Task: Add a second read replica to the existing Docker‑Compose PostgreSQL setup and modify the Node.js service to route reads to the replica pool. Then, simulate a replication lag by inserting a large batch of rows on the primary and observe the lag using pg_stat_replication.
Steps:
- Update
docker-compose.ymlto add a second replica (db-replica-2). - Configure the second replica to subscribe to the primary.
- In
db.ts, add a second replica pool (replicaPool2). - In
service.ts, modifygetUserto randomly pick betweenreplicaPoolandreplicaPool2. - Run the batch insert script on the primary.
- Query
pg_stat_replicationon the primary to see lag for each replica. - Log the lag in your application and create an alert if lag > 2 s.
11. Quick revision
- Read replicas are read‑only copies that receive changes from a primary via WAL streaming.
- Asynchronous replication gives low write latency but eventual consistency.
- Replication lag is the time difference between primary writes and replica updates.
- Use separate connection pools for reads and writes.
- Monitor
pg_stat_replicationand set alerts on lag. - Failover requires promotion of a replica; use orchestrators like Patroni.
- Replicas consume disk and network; balance cost vs. capacity.
- Do not route writes to replicas; they will reject them.
- Logical replication allows table‑level filtering and cross‑version replication.
- Use SSL/TLS for secure replication traffic.
- Load balancers (PgBouncer, HAProxy) distribute read traffic across replicas.
12. References
- PostgreSQL documentation – Replication: https://www.postgresql.org/docs/current/warm-standby.html
- PostgreSQL documentation – Logical Replication: https://www.postgresql.org/docs/current/logical-replication.html
- PgBouncer documentation: https://www.pgbouncer.org/
- Patroni documentation: https://patroni.readthedocs.io/
- Docker Compose PostgreSQL example: https://docs.docker.com/compose/compose-file/
- Prometheus PostgreSQL exporter: https://github.com/prometheus-community/postgres_exporter