--- title: "Set Up PostgreSQL Streaming Replication on Ubuntu 24.04" description: "Configure PostgreSQL 16 streaming replication on Ubuntu 24.04 with a replication slot, a read-only hot standby, lag monitoring and a tested manual failover." category: "Database Management" language: en source: https://cubepath.com/docs/database-management/postgresql-replication-configuration updated: 2026-09-25 --- # Set Up PostgreSQL Streaming Replication on Ubuntu 24.04 PostgreSQL streaming replication keeps one or more standby servers continuously in sync with a primary by shipping Write-Ahead Log (WAL) records over a network connection. The standby replays those records, can serve read-only queries, and can be promoted to primary if the original server fails. In this tutorial you will set up asynchronous streaming replication between two Ubuntu 24.04 servers running PostgreSQL 16, protect it with a replication slot, verify that data flows, measure replication lag, and practice a manual failover. An optional section shows how to switch to synchronous replication. ## Prerequisites To follow this tutorial, you will need: - Two servers running Ubuntu 24.04 LTS, for example two CubePath VPS in the same location. This guide calls them **primary** and **standby**. - A non-root user with `sudo` privileges on both servers. - A private network between the servers (recommended) or public IPs. The examples use `primary_ip` (`10.0.0.10`) and `standby_ip` (`10.0.0.11`); replace them with your own addresses. - Enough disk on the standby to hold a full copy of the primary's data directory. Both servers must run the same PostgreSQL major version. Physical streaming replication does not work across major versions (for example 15 to 16). ## Step 1 - Installing PostgreSQL on both servers Ubuntu 24.04 ships PostgreSQL 16 in its main repository. Run the following on **both** servers: ```bash sudo apt update sudo apt install -y postgresql ``` Check that the cluster is running: ```bash pg_lsclusters ``` ```output Ver Cluster Port Status Owner Data directory Log file 16 main 5432 online postgres /var/lib/postgresql/16/main /var/log/postgresql/postgresql-16-main.log ``` On Ubuntu, configuration files live in `/etc/postgresql/16/main/` and data lives in `/var/lib/postgresql/16/main/`. Keep that split in mind: you will edit files in `/etc` and replace the data directory on the standby. ## Step 2 - Configuring the primary server All steps in this section run on the **primary**. PostgreSQL 16 already has sensible replication defaults (`wal_level = replica`, `max_wal_senders = 10`, `max_replication_slots = 10`), so the only mandatory change is making PostgreSQL listen on an address the standby can reach. Open the main configuration file: ```bash sudo nano /etc/postgresql/16/main/postgresql.conf ``` Find `listen_addresses`, uncomment it and set it to localhost plus the primary's private IP: ```ini listen_addresses = 'localhost,10.0.0.10' ``` While the file is open, confirm the replication settings. You do not need to change them, but make sure nobody has lowered them: ```ini wal_level = replica max_wal_senders = 10 max_replication_slots = 10 hot_standby = on ``` Save and close the file. ### Creating the replication role Create a dedicated role that can only replicate, not log in to databases. The `\password` command prompts for the password so it never ends up in your shell history: ```bash sudo -u postgres psql ``` ```sql CREATE ROLE replicator WITH REPLICATION LOGIN; \password replicator \q ``` Choose a strong password and keep it at hand; you will use it on the standby. ### Allowing the standby to connect Replication connections are matched against the special `replication` database name in `pg_hba.conf`. Open the file: ```bash sudo nano /etc/postgresql/16/main/pg_hba.conf ``` Add this line at the end, restricting access to the standby's IP: ```text host replication replicator 10.0.0.11/32 scram-sha-256 ``` Restart PostgreSQL to apply `listen_addresses` (a reload is not enough for that setting): ```bash sudo systemctl restart postgresql ``` Confirm PostgreSQL now listens on the private IP: ```bash sudo ss -ltnp | grep 5432 ``` ```output LISTEN 0 200 10.0.0.10:5432 0.0.0.0:* users:(("postgres",pid=4121,fd=7)) LISTEN 0 200 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=4121,fd=6)) ``` ### Opening the firewall If UFW is active, allow port 5432 only from the standby: ```bash sudo ufw allow from 10.0.0.11 to any port 5432 proto tcp sudo ufw status ``` ## Step 3 - Preparing the standby The following steps run on the **standby**. A standby must start from an exact copy of the primary's data directory, so the empty cluster created by the package has to go. Stop PostgreSQL and remove its data directory: ```bash sudo systemctl stop postgresql sudo rm -rf /var/lib/postgresql/16/main ``` > Warning: Run the `rm` command only on the standby. It deletes every database in that cluster. Store the replication password in a `.pgpass` file owned by the `postgres` user, so neither `pg_basebackup` nor the running standby needs the password in a config file: ```bash sudo -u postgres bash -c 'echo "10.0.0.10:5432:replication:replicator:your_strong_password" > ~/.pgpass && chmod 600 ~/.pgpass' ``` Replace `your_strong_password` with the password you set in Step 2. Test the connection before copying anything: ```bash sudo -u postgres psql "host=10.0.0.10 user=replicator dbname=replication replication=true" -c "IDENTIFY_SYSTEM;" ``` ```output systemid | timeline | xlogpos | dbname ---------------------+----------+-----------+-------- 7412345678901234567 | 1 | 0/3000148 | (1 row) ``` If this fails, fix connectivity, `pg_hba.conf` or the firewall before continuing (see Troubleshooting). ## Step 4 - Cloning the primary with pg_basebackup `pg_basebackup` copies the primary's data directory over a replication connection. The flags used here do the rest of the standby setup for you: - `-X stream` streams the WAL generated during the copy, so the backup is consistent. - `-C -S standby1` creates a physical replication slot named `standby1` on the primary. The slot makes the primary keep WAL until this standby has received it, so the standby can never fall too far behind to recover. - `-R` writes `standby.signal` and the `primary_conninfo` connection string into the data directory, which turns the copy into a standby. Run it as the `postgres` user: ```bash sudo -u postgres pg_basebackup -h 10.0.0.10 -U replicator \ -D /var/lib/postgresql/16/main \ -X stream -C -S standby1 -R -P ``` ```output 30832/30832 kB (100%), 1/1 tablespace ``` Check that the standby files were created: ```bash sudo ls /var/lib/postgresql/16/main/standby.signal sudo cat /var/lib/postgresql/16/main/postgresql.auto.conf ``` ```output /var/lib/postgresql/16/main/standby.signal # Do not edit this file manually! # It will be overwritten by the ALTER SYSTEM command. primary_conninfo = 'user=replicator passfile=''/var/lib/postgresql/.pgpass'' channel_binding=prefer host=10.0.0.10 port=5432 sslmode=prefer ...' primary_slot_name = 'standby1' ``` ### Naming the standby The primary identifies each standby by its `application_name`, which defaults to the standby's `cluster_name`. Ubuntu sets that to `16/main` on every server, so give the standby a meaningful name. Open its configuration: ```bash sudo nano /etc/postgresql/16/main/postgresql.conf ``` Set: ```ini cluster_name = 'standby1' ``` This makes monitoring easier and is required if you later enable synchronous replication. ## Step 5 - Starting the standby and verifying replication Start PostgreSQL on the **standby**: ```bash sudo systemctl start postgresql ``` Look at the log to confirm it connected to the primary: ```bash sudo tail -n 5 /var/log/postgresql/postgresql-16-main.log ``` ```output LOG: entering standby mode LOG: redo starts at 0/4000028 LOG: consistent recovery state reached at 0/4000100 LOG: database system is ready to accept read-only connections LOG: started streaming WAL from primary at 0/5000000 on timeline 1 ``` On the **primary**, `pg_stat_replication` shows one row per connected standby: ```bash sudo -u postgres psql -x -c "SELECT application_name, client_addr, state, sync_state, replay_lag FROM pg_stat_replication;" ``` ```output -[ RECORD 1 ]----+---------- application_name | standby1 client_addr | 10.0.0.11 state | streaming sync_state | async replay_lag | 00:00:00.000912 ``` `state = streaming` means the standby is receiving WAL in real time. On the **standby**, confirm it is in recovery mode: ```bash sudo -u postgres psql -c "SELECT pg_is_in_recovery();" ``` ```output pg_is_in_recovery ------------------- t (1 row) ``` ## Step 6 - Testing that data replicates Create a test database and table on the **primary**: ```bash sudo -u postgres psql -c "CREATE DATABASE repl_test;" sudo -u postgres psql -d repl_test -c "CREATE TABLE t (id serial PRIMARY KEY, created_at timestamptz DEFAULT now()); INSERT INTO t DEFAULT VALUES;" ``` Read it from the **standby**: ```bash sudo -u postgres psql -d repl_test -c "SELECT * FROM t;" ``` ```output id | created_at ----+------------------------------- 1 | 2026-09-25 10:14:03.512391+00 (1 row) ``` Writes on the standby are rejected, which confirms it is read-only: ```bash sudo -u postgres psql -d repl_test -c "INSERT INTO t DEFAULT VALUES;" ``` ```output ERROR: cannot execute INSERT in a read-only transaction ``` Drop the test database on the primary when you are done; the drop replicates too: ```bash sudo -u postgres psql -c "DROP DATABASE repl_test;" ``` ## Step 7 - Monitoring replication lag and slots Two queries on the **primary** cover most day-to-day monitoring. The first shows how many bytes of WAL each standby still has to replay: ```bash sudo -u postgres psql -c "SELECT application_name, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS replay_lag_bytes, replay_lag FROM pg_stat_replication;" ``` ```output application_name | replay_lag_bytes | replay_lag ------------------+------------------+----------------- standby1 | 0 bytes | 00:00:00.000874 ``` The second shows how much WAL each replication slot is holding back. A slot whose standby is gone keeps WAL forever and can fill the primary's disk, so watch this value: ```bash sudo -u postgres psql -c "SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal FROM pg_replication_slots;" ``` ```output slot_name | active | retained_wal -----------+--------+-------------- standby1 | t | 56 bytes ``` To put an upper bound on that risk, set `max_slot_wal_keep_size` in `/etc/postgresql/16/main/postgresql.conf` on the primary (for example `max_slot_wal_keep_size = 20GB`) and reload with `sudo systemctl reload postgresql`. If a standby falls behind by more than that, its slot is invalidated and the standby must be rebuilt with `pg_basebackup`, but the primary stays up. Feed these two queries into your existing monitoring system (Prometheus `postgres_exporter`, Zabbix, or similar) and alert when `active` is false or the lag keeps growing. ## Step 8 - Promoting the standby (manual failover) If the primary fails, promote the standby so it accepts writes. Run this on the **standby**: ```bash sudo -u postgres psql -c "SELECT pg_promote();" ``` ```output pg_promote ------------ t (1 row) ``` Verify it left recovery mode: ```bash sudo -u postgres psql -c "SELECT pg_is_in_recovery();" ``` ```output pg_is_in_recovery ------------------- f (1 row) ``` Then point your applications to the new primary. Two points matter after a failover: - **Never let the old primary come back as a writable server.** If both accept writes, the data diverges. Keep it stopped until you rebuild it. - **Rebuild the old primary as a standby** of the new one by repeating Steps 3 to 5 with the roles reversed (update `pg_hba.conf`, the firewall and `.pgpass` accordingly). `pg_rewind` can speed this up for large databases, but it requires `wal_log_hints = on` or data checksums to have been enabled beforehand. For automatic failover, use a cluster manager such as Patroni instead of scripting promotion yourself. ## Optional - Enabling synchronous replication With asynchronous replication, a transaction committed on the primary can be lost if the primary crashes before the standby receives it. Synchronous replication makes the primary wait for the standby to confirm each commit, at the cost of higher write latency and of writes blocking if the standby is down. On the **primary**, edit `/etc/postgresql/16/main/postgresql.conf`: ```ini synchronous_standby_names = 'FIRST 1 (standby1)' synchronous_commit = on ``` Reload PostgreSQL: ```bash sudo systemctl reload postgresql ``` Check that the standby is now synchronous: ```bash sudo -u postgres psql -c "SELECT application_name, sync_state FROM pg_stat_replication;" ``` ```output application_name | sync_state ------------------+------------ standby1 | sync ``` > Important: With a single synchronous standby, every write on the primary waits if that standby is unavailable. Use at least two standbys with `ANY 1 (standby1, standby2)` if you need synchronous durability without that single point of failure. ## Troubleshooting **`no pg_hba.conf entry for replication connection`**: the `pg_hba.conf` line on the primary is missing, uses the wrong IP, or was added without a reload. Check it and run `sudo systemctl reload postgresql`. **`connection refused` or timeout from the standby**: PostgreSQL is not listening on the private IP (check `listen_addresses` and restart) or UFW blocks the port (`sudo ufw status`). **`password authentication failed for user "replicator"`**: the password in `~postgres/.pgpass` on the standby does not match, or the file is not `600`. PostgreSQL ignores a `.pgpass` with looser permissions. **`requested WAL segment ... has already been removed`**: the standby fell behind and the WAL it needs is gone, usually because no slot was used or the slot was invalidated by `max_slot_wal_keep_size`. Rebuild the standby from Step 3. **Standby log shows `database system identifier differs`**: the standby's data directory did not come from this primary. Remove it and run `pg_basebackup` again. ## Conclusion You now have a PostgreSQL 16 primary streaming WAL to a read-only hot standby through a replication slot, with queries to monitor lag and retained WAL and a tested promotion procedure. The standby can already take read-only traffic such as reports and analytics. As next steps, consider adding a second standby for synchronous durability, managing failover automatically with Patroni, and encrypting replication traffic with TLS (`ssl = on` on the primary and `hostssl` entries in `pg_hba.conf`) if it crosses an untrusted network.