PostgreSQLXIV · Replication, Slots and Read ReplicasReplication
Building a standby
What you'll learn
- Build a streaming standby from scratch
- Verify it is actually replicating rather than merely running
- Choose between a slot and an archive for catch-up safety
- Avoid the procedural mistakes that produce a broken standby
Prerequisites
Practice
Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27
Building a standby is five steps. Verifying one is four checks, and the checks matter more, because a standby that starts and does not replicate looks exactly like a standby that works.
On the primary
-- 1. a role that can stream WAL and nothing else
CREATE ROLE repl WITH REPLICATION LOGIN PASSWORD '...';
-- 2. a slot, so the primary retains WAL this standby has not consumed
SELECT pg_create_physical_replication_slot('standby1');
# 3. pg_hba.conf, above any reject rules
host replication repl 10.0.0.0/24 scram-sha-256
# 4. postgresql.conf. wal_level=replica is the default in 18.
wal_level = replica
max_wal_senders = 10 # default 10; one per standby plus headroom
max_replication_slots = 10
wal_log_hints = on # required for pg_rewind later (Part XV)
max_wal_senders needs headroom beyond the standby count, because
pg_basebackup -Xstream consumes one for the duration of a backup, and
so does each pg_receivewal.
On the standby
$ pg_basebackup -h rbpg-prim -U repl -D /var/lib/postgresql/18/docker \
-Fp -Xstream -c fast -P -R -S standby123650/23650 kB (100%), 1/1 tablespace-Fp (plain) because a standby needs a data directory, not a tarball.
-R writes the recovery configuration. -S standby1 binds the backup
and the subsequent streaming to the slot, so no WAL is missed between
the two.
$ cat postgresql.auto.conf; ls standby.signalprimary_conninfo = 'user=repl password=replpass channel_binding=prefer
host=''rbpg-prim'' port=5432 sslmode=prefer ...'
primary_slot_name = 'standby1'
standby.signalstandby.signal is what makes it a standby. Its relationship to
recovery.signal from lesson XIII-06 is exact and worth stating: both
put the cluster into recovery, and they differ in what happens at the
end of the available WAL. recovery.signal means “recover to a target,
then stop.” standby.signal means “recover, and keep waiting for
more.” A directory containing neither starts as a normal primary and
ignores every recovery setting.
Then add whatever the standby needs locally:
listen_addresses = '*'
hot_standby = on # default on; allows read queries
hot_standby_feedback = off # lesson XIV-07
max_standby_streaming_delay = 30s # lesson XIV-07
restore_command = 'cp /archive/%f %p' # the fallback path
The four checks
Starting is not evidence. These are.
$ SELECT pg_is_in_recovery(); pg_is_in_recovery
-------------------
t$ SELECT application_name, state, sync_state FROM pg_stat_replication; application_name | state | sync_state
------------------+-----------+------------
walreceiver | streaming | async$ -- primary: INSERT INTO t DEFAULT VALUES;
-- standby:
SELECT count(*) AS rows_on_standby FROM t; rows_on_standby
-----------------
1$ INSERT INTO t DEFAULT VALUES;ERROR: cannot execute INSERT in a read-only transactionCheck 2 is the one people skip and the one that catches the most.
pg_is_in_recovery() returning true only means the standby is in
recovery — it says nothing about whether anything is arriving. A
standby replaying from an archive with a dead streaming connection
passes check 1 and fails check 2.
Slot, archive, or both
The standby needs WAL the primary has not yet recycled. Three arrangements, with different failure modes:
| Arrangement | If the standby is down for a long time | Risk |
|---|---|---|
| Slot only | Primary retains WAL indefinitely | pg_wal fills the disk |
| Archive only | Standby catches up from the archive | Archive must be working |
Slot + max_slot_wal_keep_size | Slot is invalidated at the limit | Standby needs a rebuild |
| Slot + archive | Slot capped, archive covers the gap | Most moving parts |
The last row is the robust one and lesson XIV-06 is about why the first row is dangerous.
What to take from this
- Five steps: role, slot,
pg_hba.conf, primary settings,pg_basebackup -R. - Set
wal_log_hints = onbefore you needpg_rewind, not after. standby.signalmeans “keep waiting for more WAL”;recovery.signalmeans “stop at a target”.- Four checks: in recovery, visible on the primary as
streaming, data arrives, writes refused. Check 2 is the one people skip. - Stop and confirm before replacing a data directory.
- Slot plus archive is the robust arrangement.
max_connectionsand friends must be at least as large on the standby — raise them there first.
Cross-course references
- Ansible for Production Sysadmins — Part XII (Idempotency) covers building a standby the same way twice, which is what makes a rebuild during an incident survivable.
- Secrets, PKI & Certificate Management — Part XIII (Dynamic credentials) covers the replication credential, and Part VII (TLS for operators) covers encrypting the stream it authenticates.
Quiz
Knowledge check · 6 questions
Q1. A standby returns true from pg_is_in_recovery() and serves queries with data that is hours old. What check would have caught this?
Q2. Why should wal_log_hints be enabled when a standby is first built rather than when pg_rewind is needed?
Q3. max_connections is raised from 200 to 500 on a primary. The standby is left at 200. When does this become a problem?
Q4. Which settings must be at least as large on a standby as on its primary? Select all that apply.
Q5. standby.signal and recovery.signal both put a cluster into recovery, and differ in what happens when the available WAL runs out.
Q6. Compare a replication slot and a WAL archive as the mechanism that lets a standby catch up, and say why using both is usually right.
Passing score: 75%. Answers are checked in this browser.