Prompt
How do I set up read replicas in PostgreSQL for a production app?
Latest observation
Setting up PostgreSQL read replicas for a production app usually means creating one primary (read/write) server and one or more standby servers that continuously replay WAL changes from the primary. For production, the main goals are:
- protect the primary from heavy read traffic
- improve read scalability
- provide disaster recovery / failover options
- minimize replication lag and operational risk
Here’s a practical production-oriented approach.
1) Choose the replication model
PostgreSQL supports two common patterns:
Physical replication
- Copies the entire data cluster at the block/WAL level.
- Most common for read replicas.
- Simple and fast.
- Replica is usually read-only.
Logical replication
- Replicates selected tables or changes.
- Useful for partial replication, migrations, cross-version scenarios.
- More flexible, but more operational overhead.
For most production read-replica setups, physical streaming replication is the right choice.
2) Basic architecture
Typical setup:
- Primary
- accepts writes
- streams WAL to replicas
- Read replicas
- receive WAL
- replay changes
- serve read-only queries
- Optional:
- load balancer / router for read traffic
- monitoring for lag and health
- failover manager for automatic promotion
3) Configure the primary
You need to enable replication-related settings in postgresql.conf or your managed service settings.
Common parameters:
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
hot_standby = on
wal_keep_size = 1GB
Notes:
wal_level = replicais required for streaming replication.max_wal_senderscontrols concurrent replica connections.wal_keep_sizehelps prevent replicas from falling too far behind.hot_standbyis for replicas, but setting is common in cluster configs.- If you use replication slots, you may not need a large
wal_keep_size, but you must monitor disk usage carefully.
Allow replica connections
In pg_hba.conf, allow the replica host to connect:
host replication repl_user 10.0.0.0/24 scram-sha-256
Then create a replication user:
CREATE ROLE repl_user WITH REPLICATION LOGIN PASSWORD 'strong-password';
Reload PostgreSQL after changes:
SELECT pg_reload_conf();
or restart if needed.
4) Take a base backup for the replica
A replica needs an initial copy of the primary data directory.
Use pg_basebackup:
pg_basebackup \
-h primary-db.example.com \
-U repl_user \
-D /var/lib/postgresql/data \
-Fp -Xs -P -R
What these mean:
-h: primary host-U: replication user-D: target data directory-Fp: plain format-Xs: stream WAL during backup-P: progress-R: write replica connection info automatically
If you’re managing it manually, you can also configure standby connection settings yourself.
5) Start the replica
On the standby server:
- ensure PostgreSQL version matches the primary
- make sure the data directory is initialized from the base backup
- start PostgreSQL
The replica should connect to the primary and begin streaming WAL.
Check status:
SELECT * FROM pg_stat_wal_receiver;
On the primary, check connected replicas:
SELECT * FROM pg_stat_replication;
6) Make sure reads go to replicas only
Your application must route traffic carefully.
Common patterns
A. Separate read and write connection pools
- writes -> primary
- reads -> replicas
- easiest and most common in application code
B. Use a database proxy/router
Examples:
- PgBouncer
- HAProxy
- AWS RDS Proxy
- custom service discovery / DNS
Note: PgBouncer is great for pooling, but it does not automatically route reads vs writes by itself. You usually still need app logic or a router.
C. ORM-level read/write splitting
Some frameworks support this directly:
- Django database routers
- Rails multiple databases
- Hibernate read/write routing
- Sequelize/TypeORM patterns, depending on usage
7) Handle replication lag correctly
Replicas are usually asynchronous by default, which means:
- writes commit on primary first
- replicas catch up shortly after
- reads from replicas may briefly be stale
This matters for:
- user just changed profile, then immediately refreshes page
- checkout / payments / critical workflows
- any “read your own writes” requirement
Common solutions
- read from primary immediately after a write
- add a short “stickiness” window to the primary
- use synchronous replication for critical data, selectively if needed
- display slightly stale data only where acceptable
You can monitor lag with:
SELECT
pid,
application_name,
state,
sync_state,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication;
And on replica:
SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;
8) Use replication slots carefully
Replication slots prevent WAL segments from being removed before a replica consumes them.
This is useful because:
- replicas won’t miss required WAL
- easier to avoid broken replication after brief outages
But the risk is:
- if a replica dies or lags forever, WAL can pile up and fill disk
Create a slot:
SELECT pg_create_physical_replication_slot('replica1');
Use it in the standby configuration.
For production, replication slots are often a good idea, but you must actively monitor them.
9) Plan for failover
Read replicas are not the same as automatic high availability unless you design for it.
If the primary fails, you may need to:
- promote a replica
- redirect application writes
- rebuild old primary as a replica later
Manual promotion
On the replica:
pg_ctl promote -D /var/lib/postgresql/data
Or:
SELECT pg_promote();
Automated failover tools
Consider:
- Patroni
- repmgr
- Pacemaker/Corosync
- cloud-managed HA features
For production, automatic failover is strongly recommended if downtime matters.
10) Monitor the setup
At minimum, monitor:
- replica lag
- WAL sender/receiver health
- disk space on primary and replicas
- replication slot backlog
- query latency on replicas
- connection counts
- failover state
Useful views:
pg_stat_replicationpg_stat_wal_receiverpg_replication_slots
11) Production best practices
Use at least one replica in another failure domain
If possible:
- different AZ
- different rack
- different machine / subnet
Keep versions consistent
- same PostgreSQL major version for physical replication
- replicas should usually match the primary closely
Test failover regularly
Don’t assume it works until you’ve rehearsed:
- promotion
- DNS or load balancer switch
- app reconnect behavior
- recovery of old primary
Don’t put everything on replicas
Avoid using replicas for:
- critical transactional reads that must be fully fresh
- advisory locks
- session state
- any workload sensitive to staleness
Be careful with long-running queries
Replica replay can be delayed by heavy queries, and hot standby conflicts may cancel queries depending on settings.
12) Example simple production checklist
- Enable WAL streaming on primary
- Create replication user
- Allow replica IPs in
pg_hba.conf - Initialize replica with
pg_basebackup - Start replica and verify streaming
- Route reads only to replicas
- Monitor lag and disk usage
- Set up failover tooling
- Test promotion and recovery
- Document your runbook
13) If you’re using a managed service
If you’re on:
- AWS RDS / Aurora
- Google Cloud SQL
- Azure Database for PostgreSQL
- DigitalOcean Managed PostgreSQL
then replicas are often provisioned through the provider console/CLI, and many replication details are managed for you. You still need to handle:
- read/write routing
- lag awareness
- failover behavior
- app reconnection logic
14) Quick recommended approach for most production apps
If you want a safe default:
- use physical streaming replicas
- use one or more replicas in separate AZs
- route read-heavy, non-critical reads to replicas
- keep write-after-read-sensitive flows on the primary
- use monitoring + failover automation like Patroni/repmgr or your managed provider’s HA
- add replication slots only if you can monitor them properly
If you want, I can give you:
- a step-by-step setup guide on Linux with exact config files,
- a Docker Compose demo, or
- a cloud-specific guide for AWS RDS, GCP Cloud SQL, or Azure.