Overview
Postgres has two replication models. Streaming (physical) replication ships WAL to a byte-identical replica. Logical replication decodes WAL into row changes for selected tables. They solve different problems: pick streaming for read scale and failover, logical for partial copies and low-downtime upgrades. The umbrella rules live in postgres and the production playbook in postgres-prod.
Use streaming replication for read replicas and standbys
The primary ships WAL to replicas, which apply it in order. It carries every change, including DDL. The defaults (wal_level = replica, max_wal_senders = 10) already allow it.
pg_basebackup -h primary -D /var/lib/postgresql/data -R -C -S replica1 -X stream-R writes the recovery configuration, and -C -S creates the replication slot replica1. The slot makes the primary keep the WAL the replica still needs, so a disconnected replica does not fall off the retention window. Use it for a read replica, a failover candidate, or a base for backups.
Bound the WAL a slot can retain
A slot whose consumer is gone keeps WAL forever and can fill the primary’s disk. Set max_slot_wal_keep_size to cap retention (the default is unlimited). Postgres 18 adds idle_replication_slot_timeout to invalidate slots that stay inactive. Alert on inactive slots in pg_replication_slots, and drop slots for decommissioned replicas.
Use logical replication for selective copies and upgrades
Logical replication uses publications and subscriptions. It crosses major versions, copies only the tables you publish, and allows writable subscribers.
-- primary
CREATE PUBLICATION analytics FOR TABLE orders, order_items;
-- subscriber
CREATE SUBSCRIPTION analytics_sub
CONNECTION 'host=primary dbname=app user=replica password=...'
PUBLICATION analytics;Use it for low-downtime major-version upgrades, shipping tables to a reporting store, and sharding migrations. Know the limits in Postgres 18:
- DDL is not replicated; apply schema changes to both sides yourself.
- Sequence data is not replicated. Identity and serial columns copy as table data, but the subscriber’s sequence still shows the start value.
- Large objects are not replicated, and only tables (including partitioned tables) can be published.
- Stored generated columns replicate when you set
publish_generated_columnsor list them in a column list; virtual generated columns do not. - The subscription
streamingoption (14 and later) sends long in-progress transactions to the subscriber instead of spilling them to disk on the publisher until commit. Its default isoffbefore Postgres 18 andparallelin 18, which applies them with parallel apply workers.
Since Postgres 17, logical slots can fail over to a standby, and pg_upgrade carries valid logical slots and subscriptions forward from a 17 or later cluster.
Treat replicas as read scale, not backups
A DROP TABLE on the primary replays on the replica within seconds, and a corrupted page replicates as corruption. Recover from operator error with base backups plus archived WAL (point-in-time recovery); see postgres-prod. Postgres 17 added incremental base backups (pg_basebackup --incremental, pg_combinebackup). Use replicas for reporting, search indexing, and failover.
Monitor lag in bytes and seconds
-- on the primary
SELECT application_name,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes,
write_lag, flush_lag, replay_lag
FROM pg_stat_replication;Alert when replay_lag exceeds the staleness your reads can tolerate, and alert on lag_bytes growth for failover candidates. Wire both into observability before the first stale-read incident.
Use async by default and sync per transaction
Async commits do not wait for replicas, so a primary failure can lose up to the lag. With synchronous_standby_names set, a synchronous commit waits for the named standbys and adds a round trip. Request it per transaction for the few writes that need it, because a global setting stalls every commit when the sync standby disconnects.
SET LOCAL synchronous_commit = remote_apply; -- visible to queries on the standbyremote_write is the cheapest level, on waits for the standby’s flush, and remote_apply is the strongest.
Trade hot_standby_feedback against bloat
A long replica query can conflict with vacuum on the primary. hot_standby_feedback = on makes the primary keep the rows the replica still needs: no cancellations, but bloat on the primary (postgres-vacuum). Keep it off on OLTP read replicas; turn it on for a dedicated analytics replica.
Drill the failover
Postgres has no built-in automatic failover; use Patroni or your provider’s. The manual path: promote with pg_ctl promote or pg_promote(), repoint the pool, re-clone the old primary as a replica. Rehearse it quarterly. A topology that has never failed over is theoretical.