PostgreSQL Streaming Replication
Set up PostgreSQL 16 streaming replication with a primary and hot standby, covering WAL configuration, pg_basebackup, sync vs async modes, and manual failover.
Before you start
- ▸Two servers (or VMs) with network connectivity between them on port 5432
- ▸PostgreSQL 16 installed on both nodes from the PGDG repository
- ▸Root or sudo access on both nodes
- ▸Clocks synchronized via NTP/chrony on both nodes
PostgreSQL streaming replication lets a standby server replay WAL (Write-Ahead Log) records from a primary in near real-time, giving you a hot standby you can fail over to with minimal data loss. This guide walks through a complete two-node setup: one primary and one standby, using PostgreSQL 16 on current LTS releases. The same steps apply to PostgreSQL 14 and 15 with minor version-number substitutions.
Architecture Overview
The primary continuously ships WAL segments to the standby via a dedicated replication connection. The standby runs in recovery mode, replaying those WAL records and optionally serving read-only queries. Two modes exist:
- Asynchronous replication (default): The primary commits before confirming the standby received the WAL. Fastest, but a crash can lose the last few transactions.
- Synchronous replication: The primary waits for the standby to confirm WAL receipt (or apply, depending on
synchronous_commitlevel) before returning to the client. Zero data loss, but adds round-trip latency to every write.
In this guide, both nodes are assumed to be on the same network. Use the real IP addresses for your environment. We will use 192.168.1.10 as the primary and 192.168.1.20 as the standby throughout.
Prerequisites and Installation
Install PostgreSQL 16 on both nodes before starting. Use the PostgreSQL Global Development Group (PGDG) repository for the freshest packages.
Debian / Ubuntu
sudo apt install -y curl ca-certificates
sudo install -d /usr/share/postgresql-common/pgdg
curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
--fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
sudo sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] \
https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list'
sudo apt update && sudo apt install -y postgresql-16
Fedora / RHEL 9 / Rocky 9
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo dnf -qy module disable postgresql
sudo dnf install -y postgresql16-server postgresql16-contrib
sudo /usr/pgsql-16/bin/postgresql-16-setup initdb
Arch Linux
sudo pacman -S postgresql
sudo -u postgres initdb --locale=en_US.UTF-8 -D /var/lib/postgres/data
Start and enable PostgreSQL on the primary only for now:
# Debian/Ubuntu
sudo systemctl enable --now postgresql@16-main
# Fedora/RHEL/Rocky
sudo systemctl enable --now postgresql-16
# Arch
sudo systemctl enable --now postgresql
Step 1 — Create the Replication Role
On the primary, create a dedicated PostgreSQL role for replication. Never use the postgres superuser for this.
sudo -u postgres psql -c \
"CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'StrongPassw0rd!'"
Step 2 — Configure the Primary
Edit postgresql.conf on the primary. The file lives at /etc/postgresql/16/main/postgresql.conf on Debian/Ubuntu, /var/lib/pgsql/16/data/postgresql.conf on RHEL/Rocky, and /var/lib/postgres/data/postgresql.conf on Arch.
# Key settings — append or edit these lines
listen_addresses = '*' # accept connections from the standby
wal_level = replica # minimum for streaming replication
max_wal_senders = 5 # concurrent WAL sender processes
wal_keep_size = 256 # MB of WAL to retain for lagging standbys
hot_standby = on # allows read queries on the standby
archive_mode = off # enable and configure if you also want PITR
For synchronous replication, add the standby's application name to synchronous_standby_names. Skip this for async:
synchronous_standby_names = 'standby1'
synchronous_commit = remote_apply # strongest guarantee; use 'on' for slightly less
Allow the Standby in pg_hba.conf
Add a replication entry to pg_hba.conf (same directory as postgresql.conf):
# TYPE DATABASE USER ADDRESS METHOD
host replication replicator 192.168.1.20/32 scram-sha-256
Reload the primary to apply changes:
sudo systemctl reload postgresql@16-main # Debian/Ubuntu
sudo systemctl reload postgresql-16 # RHEL/Rocky/Fedora
sudo systemctl reload postgresql # Arch
Step 3 — Open the Firewall on the Primary
ufw (Ubuntu/Debian)
sudo ufw allow from 192.168.1.20 to any port 5432
firewalld (Fedora / RHEL / Rocky)
sudo firewall-cmd --permanent --add-rich-rule=\
'rule family=ipv4 source address=192.168.1.20 port port=5432 protocol=tcp accept'
sudo firewall-cmd --reload
Step 4 — Bootstrap the Standby with pg_basebackup
On the standby, stop PostgreSQL if it started automatically, then wipe the data directory and replace it with a base backup from the primary. This is destructive — any existing data on the standby is lost.
sudo systemctl stop postgresql@16-main # or the distro-appropriate unit
sudo rm -rf /var/lib/postgresql/16/main/* # Debian/Ubuntu path; adjust for your distro
sudo -u postgres pg_basebackup \
-h 192.168.1.10 \
-U replicator \
-D /var/lib/postgresql/16/main \
-P -Xs -R
# -Xs : stream WAL during the backup
# -R : write standby.signal and connection info into postgresql.auto.conf
The -R flag creates the standby.signal file and adds a primary_conninfo entry automatically. Verify both exist:
ls /var/lib/postgresql/16/main/standby.signal
grep primary_conninfo /var/lib/postgresql/16/main/postgresql.auto.conf
For synchronous replication, set the application name so the primary can identify this standby:
sudo -u postgres sh -c "echo \"primary_conninfo = 'host=192.168.1.10 port=5432 \
user=replicator password=StrongPassw0rd! application_name=standby1'\" \
>> /var/lib/postgresql/16/main/postgresql.auto.conf"
Step 5 — Start the Standby
sudo systemctl start postgresql@16-main # Debian/Ubuntu
sudo systemctl start postgresql-16 # RHEL/Rocky/Fedora
sudo systemctl start postgresql # Arch
Step 6 — Verify Replication
On the primary, check the pg_stat_replication view. You should see one row for the standby.
sudo -u postgres psql -c \
"SELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, sync_state \
FROM pg_stat_replication;"
Expected output (values will differ):
client_addr | state | sent_lsn | write_lsn | flush_lsn | replay_lsn | sync_state
----------------+-----------+-----------+-----------+-----------+------------+------------
192.168.1.20 | streaming | 0/5000060 | 0/5000060 | 0/5000060 | 0/5000060 | async
For sync replication the sync_state column shows sync instead of async. On the standby, confirm it is in recovery:
sudo -u postgres psql -c "SELECT pg_is_in_recovery();"
Failover
PostgreSQL does not ship an automatic failover manager — use repmgr or Patroni for production. For a manual failover:
- Confirm the primary is genuinely down (avoid split-brain).
- On the standby, promote it to primary:
sudo -u postgres pg_ctl promote -D /var/lib/postgresql/16/main
# Or via SQL on the standby:
sudo -u postgres psql -c "SELECT pg_promote();"
After promotion, standby.signal is removed automatically and the node accepts writes. Update your application connection string to point to the new primary. The old primary, once recovered, must be re-basebackuped as a new standby — it cannot simply rejoin without a fresh base backup unless you use rewind (pg_rewind).
Using pg_rewind to Rejoin the Old Primary
pg_rewind rewinds the old primary's data directory to a point it diverged from the new primary, avoiding a full base backup. Requires wal_log_hints = on or data checksums enabled on both nodes before the failure.
sudo -u postgres pg_rewind \
--target-pgdata=/var/lib/postgresql/16/main \
--source-server="host=192.168.1.20 user=postgres dbname=postgres"
Then add standby.signal and update primary_conninfo to point to the new primary (192.168.1.20), and restart PostgreSQL.
Troubleshooting
- Standby stuck in "startup" state: Check
pg_logon the standby for authentication errors. Verifypg_hba.confon the primary allows the standby IP under thereplicationdatabase entry. - WAL sender count exhausted: Increase
max_wal_senderson the primary and reload. Each standby and eachpg_basebackupsession consumes one slot. - Replication lag growing: The standby cannot keep up with WAL generation. Investigate I/O performance on the standby. Consider replication slots (
pg_create_physical_replication_slot) to prevent WAL being discarded before the standby consumes it — but monitor slot lag closely to avoid disk exhaustion on the primary. - synchronous_commit and client timeouts: If your standby goes offline under sync replication, writes on the primary will block indefinitely until the standby reconnects or you set
synchronous_standby_names = ''. Plan your HA topology accordingly. - pg_basebackup fails with "could not connect": Verify the firewall rule, confirm
listen_addressesis notlocalhost, and test connectivity withpsql -h 192.168.1.10 -U replicator -d replicationfrom the standby.
Frequently asked questions
- What is the difference between synchronous and asynchronous replication in PostgreSQL?
- In async mode (the default), the primary commits transactions before confirming the standby received the WAL, so a primary crash can lose a small amount of data. In sync mode, the primary waits for the standby to acknowledge WAL receipt or apply before returning to the client, eliminating data loss at the cost of added write latency.
- Do I need replication slots, and are they risky?
- Slots prevent the primary from discarding WAL that the standby hasn't consumed yet, which is useful if the standby can lag. The risk is that a standby that stays offline for a long time causes WAL to accumulate on the primary and fill the disk. Monitor pg_replication_slots for lag and set max_slot_wal_keep_size as a safety limit.
- Can the standby serve read queries?
- Yes. Set hot_standby = on in postgresql.conf on the standby (which pg_basebackup copies from the primary). Clients can connect and run read-only SELECT queries while the standby is in recovery mode.
- How do I automate failover for production use?
- PostgreSQL core does not include an automatic failover manager. The most widely used options are Patroni (uses etcd, Consul, or ZooKeeper for distributed consensus) and repmgr (simpler, witness-based). Both handle promotion, fencing, and rejoining old primaries automatically.
- Does this setup survive a PostgreSQL major version upgrade?
- No. Streaming replication requires both nodes to run the same major version of PostgreSQL. For major-version upgrades, you need pg_upgrade or logical replication strategies, and the standby must be upgraded independently.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.