The first time I set up PostgreSQL replication, I remember being surprised at how few moving parts there actually are once you understand the concept. It sounds intimidating — streaming WAL across servers, replication slots, standby signals — but at its core, replication is just PostgreSQL copying its write-ahead log to another server and replaying it there. In this guide, I’ll walk through exactly how to set up streaming replication from scratch, explain the settings that matter, and cover the troubleshooting steps I’ve had to use myself more times than I’d like to admit.
What Is Replication in PostgreSQL?
Replication is the process of maintaining a copy of your database on a separate server (or servers) that stays synchronized with the original. PostgreSQL supports several replication methods, but the most common and most robust is streaming replication, where the standby server connects to the primary and continuously receives WAL (write-ahead log) records as they’re generated.
There are a few flavors worth knowing about:
- Physical replication — byte-for-byte replication of the entire database cluster. This is what
pg_basebackupand streaming replication use by default. - Logical replication — replicates changes at the row level using publications and subscriptions, allowing selective replication of specific tables and even replication between different major versions.
- Synchronous vs. asynchronous replication — synchronous replication waits for the standby to confirm it has received (and optionally applied) WAL before the primary considers a transaction committed; asynchronous doesn’t wait, which is faster but risks losing the most recent transactions if the primary crashes.
This guide focuses on physical streaming replication, since it’s the foundation most PostgreSQL high-availability setups are built on.
Prerequisites
- Two servers with the same major PostgreSQL version installed.
- Network connectivity between them on port 5432 (or whatever port you’ve configured).
- Root or sudo access on both machines.
- A rough idea of your database size, since the initial base backup needs to transfer the entire dataset.
Step 1: Configure the Primary Server
Open postgresql.conf on the primary and set:
listen_addresses = '*'
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
wal_keep_size = 1024
A few notes:
listen_addresses = '*'allows connections from any IP (you’ll restrict this properly viapg_hba.confand your firewall — don’t rely on this setting alone for security).wal_level = replicais required for replication to work at all.max_replication_slotsreserves slots for replicas that want guaranteed WAL retention.
Next, edit pg_hba.conf to allow the standby to connect for replication:
host replication replicator 192.168.1.20/32 scram-sha-256
Create the replication role:
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'use_a_strong_password';
Restart PostgreSQL so wal_level, max_wal_senders, and max_replication_slots take effect — these require a full restart, not just a reload.
Step 2: Create a Replication Slot (Recommended)
Replication slots ensure the primary retains WAL until the standby has confirmed it received it, which prevents the standby from falling irrecoverably behind if it disconnects temporarily.
SELECT pg_create_physical_replication_slot('standby1_slot');
You can view existing slots with:
SELECT * FROM pg_replication_slots;
Keep in mind that if a standby using a slot goes offline for a long time, WAL will keep accumulating on the primary and can eat up disk space. Monitor this if you go this route.
Step 3: Take a Base Backup on the Standby
On the standby machine, make sure PostgreSQL is stopped and the data directory is empty, then run:
pg_basebackup -h 192.168.1.10 -D /var/lib/postgresql/16/main -U replicator -P -R -X stream -S standby1_slot
Here’s what each flag does:
-h— primary server address.-D— target data directory on the standby.-U— replication role.-P— show backup progress.-R— auto-generatestandby.signalandprimary_conninfoinpostgresql.auto.conf.-X stream— streams WAL alongside the base backup so you don’t miss changes that happen during the backup itself.-S standby1_slot— ties this standby to the replication slot you created earlier.
This step copies the entire data directory over the network, so expect it to take a while on larger databases. I like to run it with -P specifically so I can see it’s actually making progress rather than staring at a frozen terminal wondering if it’s hung.
Step 4: Verify the Standby Configuration
After pg_basebackup finishes, check that /var/lib/postgresql/16/main/standby.signal exists — this file (empty, just a marker) is what tells PostgreSQL to start in standby mode.
Check postgresql.auto.conf for a line similar to:
primary_conninfo = 'user=replicator password=use_a_strong_password host=192.168.1.10 port=5432 sslmode=prefer'
primary_slot_name = 'standby1_slot'
If you didn’t use -R, you’ll need to add these manually.
Step 5: Start the Standby
sudo systemctl start postgresql
Check the logs:
tail -f /var/log/postgresql/postgresql-16-main.log
Look for:
LOG: started streaming WAL from primary at 0/3000000 on timeline 1
LOG: consistent recovery state reached at 0/30000F8
LOG: database system is ready to accept read-only connections
Step 6: Confirm Replication Status
On the primary:
SELECT client_addr, state, sync_state, replay_lag, write_lag, flush_lag
FROM pg_stat_replication;
On the standby:
SELECT pg_is_in_recovery();
SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();
If pg_is_in_recovery() returns true and the LSN values are advancing over time, replication is working correctly.
Synchronous Replication (Optional)
If you need stronger durability guarantees — for example, in a financial system where losing even a single committed transaction is unacceptable — you can configure synchronous replication. On the primary:
synchronous_standby_names = 'standby1'
And give your standby an application_name matching this in its primary_conninfo:
primary_conninfo = 'user=replicator host=192.168.1.10 application_name=standby1 ...'
With this in place, the primary will wait for standby1 to confirm receipt of WAL before acknowledging a commit to the client. This adds latency to every write, so weigh that trade-off against your durability requirements carefully.
Common Use Cases
- Building a foundation for hot standby / high availability failover.
- Offloading read queries to reduce load on the primary.
- Feeding a reporting or analytics pipeline without touching production.
- Creating a geographically distant copy for disaster recovery.
Troubleshooting
Standby won’t connect: “FATAL: password authentication failed” — double check the password in primary_conninfo matches the replication role’s password, and that the pg_hba.conf entry on the primary uses the correct authentication method (scram-sha-256 is the modern default).
“replication slot does not exist” — you referenced a slot in primary_conninfo that was never created on the primary, or it was created and later dropped. Recreate it with pg_create_physical_replication_slot.
Base backup fails partway through with a timeout — increase statement_timeout or check for network instability between the two hosts. For very large databases, consider running the backup during low-traffic hours.
Standby is stuck and never reaches “ready to accept read-only connections” — check whether hot_standby is set to on in postgresql.conf, and confirm the standby can actually reach the primary on the replication port.
Disk filling up on the primary — almost always caused by an inactive replication slot holding onto WAL. Check pg_replication_slots for slots where active is false, and drop ones you no longer need with SELECT pg_drop_replication_slot('slot_name');.
Best Practices
- Use replication slots, but monitor them — an orphaned slot is one of the most common causes of a primary running out of disk space.
- Keep primary and standby on identical PostgreSQL versions and, ideally, similar hardware.
- Automate monitoring of
replay_lagso you know immediately if a standby starts falling behind. - Test your failover procedure regularly instead of assuming it’ll work when you actually need it.
- If you need automated failover rather than manual promotion, look into tools like Patroni, repmgr, or pg_auto_failover rather than building your own orchestration from scratch.
Wrapping Up
Replication in PostgreSQL comes down to three things: enabling WAL generation at the right level, streaming that WAL to a standby, and keeping that standby in sync. Once you’ve done it manually a couple of times, it stops feeling mysterious and starts feeling like just another piece of your infrastructure. From here, you can layer on hot standby for read queries, synchronous replication for stronger durability, or a failover manager for full high availability — but the streaming replication setup above is the foundation all of that is built on.
