
PostgreSQL streaming replication continuously copies the transaction logs (WAL) from the primary to one or more standbys. The standby can serve as a hot standby (real-time reads) and take over in case of failure through a failover (promote). This guide builds a two-node cluster from end to end, from the primary configuration to the promotion of the standby, with all the commands tested on PostgreSQL 18.
What you will learn
- Configure a primary for streaming replication (wal_level, dedicated role, slot, pg_hba)
- Initialize a standby with
pg_basebackup -Rand understandstandby.signal - Monitor replication:
pg_stat_replication,pg_stat_wal_receiver,pg_wal_lsn_diff() - Use the hot standby for reads and understand its limits
- Run a manual failover with
pg_promote()and check the timeline change - Enable synchronous replication and measure what it costs in availability
- Tell physical and logical replication apart to choose the right approach
In what context?
Streaming replication answers concrete administration needs:
- You administer a production PostgreSQL and want to eliminate the single point of failure (SPOF)
- You need a read standby to offload reporting queries from the primary
- You are preparing a planned maintenance (version upgrade, migration) with a controlled switchover
- You must guarantee a fast recovery (short RTO, that is, a low time to restore service) if the main server crashes
- You want to understand what the orchestration tools (Patroni, repmgr) automate before entrusting them with a failover
What this guide does not cover
Three neighbouring topics systematically come up as soon as replication is discussed, and mixing them leads to lopsided architectures. Here is where they are covered:
- Backup and PITR (Point-In-Time Recovery) → see the backup and restore guide (in French)
- Automatic orchestration of failover (Patroni, repmgr, pg_auto_failover) → mentioned at the end of the guide
- Detailed logical replication → only an overview at the end of the guide, the detail is out of scope here
pg_basebackup appears here as the tool that initializes the standby, not as a
backup tool.
Prerequisites
- Two servers with PostgreSQL installed (same major version)
- A network allowing a TCP connection between the two nodes (port 5432)
- The basics from the installation guide and the configuration guide
- Superuser access (
postgres) or a role withREPLICATION
Environment of this guide's examples (the lab at the end of the page uses db1.lab and db2.lab):
| Role | Hostname | IP | OS | PostgreSQL |
|---|---|---|---|---|
| Primary | pg-primary | 192.168.122.61 | Debian 12 | 18.3 |
| Standby | pg-standby | 192.168.122.62 | Debian 12 | 18.3 |
Replication is not a backup
This is a costly confusion in operations, and it comes from an understandable shortcut: "my data is on two machines, so it is protected". Replication protects against the loss of a machine, not against data loss. The two risks call for two different mechanisms, and one never replaces the other.
Physical vs logical replication
PostgreSQL offers two types of replication. This guide focuses on physical replication (streaming), the most common one for high availability.
Physical: bit-for-bit copy of the WAL
Physical replication (streaming) copies the WAL logs byte by byte. The standby is an exact copy of the primary: same structure, same databases, same data, same state.
Characteristics:
- Copies the whole cluster (all databases, all roles)
- The standby is read-only (hot standby)
- Very low latency (near real time)
- The standby can serve as the basis for a failover
Logical: selective replication by publication/subscription
Logical replication transmits changes at the row level (INSERT, UPDATE, DELETE) for chosen tables. It uses the
PUBLICATION / SUBSCRIPTION mechanism introduced in PostgreSQL 10.
Characteristics:
- Replicates specific tables, not the whole cluster
- The subscriber is an independent server (read-write)
- Allows replication between different versions of PostgreSQL
- Useful for migration, data aggregation or selective sharing
When to use which
The Main objective row is enough to decide in the vast majority of cases: if you want to survive the loss of a server, use physical replication; if you want to move or share data, use logical replication. The Different versions row is the one that decides in a particular but frequent case, the major version upgrade: physical replication requires the same major version on both sides, so it cannot be used to migrate from a PostgreSQL 16 to an 18.
| Criterion | Physical (streaming) | Logical |
|---|---|---|
| Main objective | High availability, failover | Migration, selective replication |
| Granularity | Whole cluster | Chosen tables |
| Read standby | Yes (hot standby) | The subscriber is read-write |
| Failover | Yes (pg_promote) | Not designed for it |
| Different versions | No (same major version) | Yes |
| Latency | Very low | Low to moderate |
| Complexity | Simple | More complex (conflicts possible) |
Configure the primary
Configuring the primary takes four steps: replication parameters, a dedicated role, a replication slot and network authorization.
Replication parameters
The essential parameters are in postgresql.conf. On Debian, we
use ALTER SYSTEM to modify them cleanly:
-- WAL level required for replication.-- This is already the default value: this line is there to make sure of it,-- notably if the server was switched to 'minimal' for a bulk load.ALTER SYSTEM SET wal_level = 'replica';
-- Maximum number of simultaneous replication connectionsALTER SYSTEM SET max_wal_senders = 5;
-- Maximum number of replication slotsALTER SYSTEM SET max_replication_slots = 10;
-- Amount of WAL kept for standbys that fall behindALTER SYSTEM SET wal_keep_size = '256MB';
-- Listen on all interfaces (not only localhost)ALTER SYSTEM SET listen_addresses = '*';Before reloading or restarting, read the context of each
parameter in pg_settings: the pending_restart column tells you whether a
written value is still waiting for a restart. The guide
Configure PostgreSQL
details this reading and the WAL tuning that replication depends on.
After the restart, check that the parameters are applied:
SELECT name, settingFROM pg_settingsWHERE name IN ( 'wal_level', 'max_wal_senders', 'max_replication_slots', 'wal_keep_size', 'listen_addresses'); name | setting------------------------+--------- listen_addresses | * max_replication_slots | 10 max_wal_senders | 5 wal_keep_size | 256 wal_level | replicaCreate a dedicated REPLICATION role
The standby must authenticate with the primary to open its WAL stream. Using the
postgres role (superuser) for this connection is a bad practice: the password ends up
stored on the standby side, and it would open full access to the primary if the standby were compromised. Create
a dedicated role with only the LOGIN and REPLICATION privileges:
CREATE ROLE replicator WITH LOGIN REPLICATION PASSWORD 'motdepasse_fort';By default, this role has no rights on your tables: it cannot create databases, nor modify anything in SQL. It remains sensitive, though, because it can receive the WAL stream and run pg_basebackup, and therefore obtain a complete copy of the cluster. Treat its password like that of a read access to all your data.
Create a physical replication slot
A replication slot guarantees that the primary keeps the WAL files the standby needs, even if the standby is temporarily disconnected. Without a slot, a standby disconnected for too long can lose synchronization if the WAL files have been recycled (deleted to free up space).
SELECT pg_create_physical_replication_slot('standby_slot');Check:
SELECT slot_name, slot_type, active FROM pg_replication_slots; slot_name | slot_type | active--------------+-----------+-------- standby_slot | physical | fThe slot is inactive (active = f) as long as no standby uses it.
Dropping an orphan slot by hand assumes you notice it in time. PostgreSQL provides two safeguards that make this monitoring much less critical.
Allow replication in pg_hba.conf
The pg_hba.conf file controls which connections are allowed. Add a line
to allow the replicator role from the standby's IP:
# TYPE DATABASE USER ADDRESS METHODhost replication replicator 192.168.122.62/32 scram-sha-256Reload the configuration:
sudo systemctl reload postgresqlCheck: the active rules are visible in pg_hba_file_rules:
SELECT line_number, type, database, user_name, address, netmask, auth_methodFROM pg_hba_file_rulesWHERE 'replication' = ANY(database); line_number | type | database | user_name | address | netmask | auth_method-------------+------+---------------+--------------+----------------+---------------+--------------- 117 | host | {replication} | {replicator} | 192.168.122.62 | 255.255.255.255 | scram-sha-256The host line above lets the client decide whether to encrypt or not: the
WAL stream, which contains all your data, can therefore travel in cleartext
between the two nodes. The primary_conninfo generated further down also shows
sslmode=prefer, which accepts a fallback without TLS. On a network you do not
control, replace host with hostssl on the primary side and set
sslmode=verify-full in primary_conninfo on the standby side. The guide
Control access to PostgreSQL
shows how to enforce TLS with hostssl and a dedicated certificate.
Initialize the standby with pg_basebackup
The standby is initialized by copying the entire cluster from the primary with
pg_basebackup. This command runs on the standby.
Prepare the standby
Before running pg_basebackup, the standby's data directory must be
empty. If PostgreSQL was installed and initialized automatically, empty it:
# Stop PostgreSQL on the standbysudo systemctl stop postgresql
# Empty the data directorysudo -u postgres find /var/lib/postgresql/18/main -mindepth 1 -deleteCopy the cluster with pg_basebackup -R
The -R option is crucial: it automatically generates standby.signal and
writes the primary_conninfo into postgresql.auto.conf.
sudo -u postgres pg_basebackup \ -h 192.168.122.61 \ -U replicator \ -D /var/lib/postgresql/18/main \ -Fp -Xs -P -R| Option | Role |
|---|---|
-h | Address of the primary |
-U | Replication role |
-D | Target data directory |
-Fp | Plain format (files) |
-Xs | Stream the WAL during the copy |
-P | Show progress |
-R | Generate standby.signal + primary_conninfo |
One last point before running the command: pg_basebackup runs with the rights of the local system account and trusts what the primary sends it. That trust has already been exploited.
What -R generates
Two elements are created automatically by the -R option:
1. The standby.signal file, an empty file that tells PostgreSQL to
start in standby mode:
ls -la /var/lib/postgresql/18/main/standby.signal-rw------- 1 postgres postgres 0 Apr 13 11:44 standby.signal2. The primary_conninfo parameter in postgresql.auto.conf, the connection
string to the primary:
grep primary_conninfo /var/lib/postgresql/18/main/postgresql.auto.confprimary_conninfo = 'user=replicator password=motdepasse_fort channel_binding=prefer host=192.168.122.61 port=5432 sslmode=prefer sslnegotiation=postgres sslcompression=0 sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'pg_basebackup writes all the libpq parameters (PostgreSQL's client library) with their effective value, including those you did not provide. The line is therefore long, but only user, host and password come from what you provided; the rest, port included, takes the client's default values.
One parameter remains to add, which pg_basebackup -R does not generate: the slot name. Without
it, the standby will connect to the primary just fine, but without reserving any WAL, and the slot created earlier will stay unused. Set primary_slot_name before the first start:
echo "primary_slot_name = 'standby_slot'" | \ sudo -u postgres tee -a /var/lib/postgresql/18/main/postgresql.auto.confStart the standby
At this point, the data directory contains a complete copy of the primary, the standby.signal file and the connection string. PostgreSQL detects standby.signal at startup and goes straight into recovery mode, where it replays WAL files, instead of opening the database read-write. The startup may take a few seconds longer than a normal start, the time to replay the WAL accumulated during the copy.
sudo systemctl start postgresqlCheck right away that the standby is in recovery mode:
SELECT pg_is_in_recovery(); pg_is_in_recovery------------------- tt (true) confirms that PostgreSQL is running in standby mode.
Check the replication
Once the standby has started, you need to check that streaming replication is active and working. The two key views are pg_stat_replication (on the primary) and pg_stat_wal_receiver (on the standby).
On the primary: pg_stat_replication
This view shows the active replication connections:
SELECT pid, state, sync_state, sent_lsn, write_lsn, flush_lsn, replay_lsnFROM pg_stat_replication; pid | state | sync_state | sent_lsn | write_lsn | flush_lsn | replay_lsn------+-----------+------------+-----------+-----------+-----------+------------ 4536 | streaming | async | 0/30067F0 | 0/30067F0 | 0/30067F0 | 0/30067F0| Column | Meaning |
|---|---|
state | streaming = replication active in real time |
sync_state | async = asynchronous replication (default) |
sent_lsn | Last WAL position sent to the standby |
write_lsn | Last position written by the standby (handed to the file system, not necessarily flushed to disk) |
flush_lsn | Last position flushed to disk (flush) by the standby |
replay_lsn | Last position replayed by the standby |
When the four LSN columns (Log Sequence Number, the position in the WAL stream) are identical, the standby is perfectly synchronized.
On the standby: pg_stat_wal_receiver
This view shows the active replication connection on the standby side:
SELECT status, sender_host, sender_port, slot_name, flushed_lsnFROM pg_stat_wal_receiver; status | sender_host | sender_port | slot_name | flushed_lsn-----------+----------------+-------------+--------------+------------- streaming | 192.168.122.61 | 5432 | standby_slot | 0/30067F0status = streaming confirms that the standby receives the WAL continuously.
Also check the slot_name column: if it is empty, the standby
is not using the slot you created on the primary, because
primary_slot_name was not set. Replication works
anyway, but you lose the WAL retention guarantee and the slot will stay
active = f on the primary.
Compute the lag with pg_wal_lsn_diff()
To measure the replication lag, that is, how far the standby is behind the primary, in bytes:
-- On the primarySELECT client_addr, pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replay_lag_bytesFROM pg_stat_replication; client_addr | replay_lag_bytes-----------------+------------------ 192.168.122.62 | 0A lag of 0 bytes means that the standby has replayed everything the primary sent it. In production under load, a slight lag (a few KB) is normal.
Test the replication in real time
The most telling test: write on the primary, read on the standby.
On the primary, insert a row:
INSERT INTO app.servers (hostname, ip_address, os, role, env, cpu_cores, ram_gb)VALUES ('srv-repl-test', '10.0.0.42', 'Debian 12', 'test', 'dev', 2, 4);On the standby, check immediately:
SELECT hostname, ip_address FROM app.serversWHERE hostname = 'srv-repl-test'; hostname | ip_address---------------+------------ srv-repl-test | 10.0.0.42The row is visible almost instantly. The standby receives and replays the WAL continuously.
Hot standby: reads on the replica
The hot standby lets you run read queries on a standby
while it keeps receiving and replaying the WAL. The hot_standby parameter
controls this feature, it is on by default
since PostgreSQL 10 and only has an effect on a server in recovery/standby.
No configuration is needed on the primary.
What you can do
The hot standby accepts read-only operations:
-- SELECT queriesSELECT * FROM app.servers WHERE env = 'prod';
-- Statistics viewsSELECT * FROM pg_stat_user_tables;
-- Execution plansEXPLAIN ANALYZE SELECT * FROM app.servers WHERE hostname LIKE 'srv%';It is ideal for:
- Reporting and long analytical queries
- Distributed reads (connect the read applications to the standby)
- Debugging and performance analysis without impacting the primary
What you cannot do
Any write operation is refused:
-- INSERT → errorINSERT INTO app.servers (hostname) VALUES ('test');ERROR: cannot execute INSERT in a read-only transaction-- DDL → errorCREATE TABLE app.test (id serial);ERROR: cannot execute CREATE TABLE in a read-only transactionThis is normal and expected behaviour: the standby is a read-only copy of the primary.
Hot standby conflicts
Sometimes, the replay of the WAL on the standby comes into conflict with long read queries. For example, if the primary runs a VACUUM that removes dead rows (the old versions of rows that have become useless; the role of VACUUM is detailed in the guide
Monitor and maintain PostgreSQL),
the standby must replay this operation, but a SELECT query
may be reading those rows.
The max_standby_streaming_delay parameter controls how long the standby
waits before cancelling the conflicting query:
SHOW max_standby_streaming_delay; max_standby_streaming_delay----------------------------- 30sBy default, the standby waits 30 seconds before cancelling the conflicting query. Raise this value if your reporting queries are long:
ALTER SYSTEM SET max_standby_streaming_delay = '120s';SELECT pg_reload_conf();Manual failover
Failover means promoting the standby to be the new primary when the old primary is unavailable (failure, maintenance, migration).
Check the state before the promote
Before promoting, make sure the standby is properly synchronized:
-- On the standby: check that we are in recoverySELECT pg_is_in_recovery();-- → t
-- Check the current timelineSELECT timeline_id FROM pg_control_checkpoint();-- → 1
-- Check that standby.signal exists-- ls -la /var/lib/postgresql/18/main/standby.signalPromote with pg_promote()
The promotion is done with a single SQL command, run on the standby:
SELECT pg_promote(); pg_promote------------ tCheck the promote
After the promote, several changes are immediately observable on the former standby:
1. No longer in recovery:
SELECT pg_is_in_recovery(); pg_is_in_recovery------------------- ff (false): the server is no longer a standby, it accepts writes.
2. New timeline:
SELECT timeline_id FROM pg_control_checkpoint(); timeline_id------------- 2The timeline goes from 1 to 2. Each promote creates a new timeline to avoid any confusion with the WAL of the old one.
3. standby.signal removed:
ls /var/lib/postgresql/18/main/standby.signalls: cannot access '/var/lib/postgresql/18/main/standby.signal': No such file or directoryPostgreSQL automatically deleted the file.
4. pg_stat_wal_receiver empty:
SELECT * FROM pg_stat_wal_receiver;(0 rows)The standby no longer receives WAL, it is now an autonomous primary.
5. Writes work:
INSERT INTO app.servers (hostname, ip_address, os, role, env, cpu_cores, ram_gb)VALUES ('srv-after-promote', '10.0.0.99', 'Debian 12', 'test', 'dev', 2, 4);INSERT 0 1The INSERT goes through, and so does DDL. The server is fully operational in read-write mode.
What to do with the old primary?
After the promote, the old primary is still on timeline 1 and does not know the standby was promoted. There are three options:
| Option | When to use it |
|---|---|
| Full pg_basebackup | The old primary has diverged a lot or is corrupted |
| pg_rewind | The old primary has diverged little (recommended) |
| Rebuild completely | Change of version or configuration |
pg_rewind is the most efficient option when the old primary accepted only a few writes after the standby disconnected:
# Stop the old primarysudo systemctl stop postgresql
# Resynchronize with the new primarysudo -u postgres pg_rewind \ --target-pgdata=/var/lib/postgresql/18/main \ --source-server="host=192.168.122.62 user=postgres dbname=postgres"Then, create a standby.signal and configure primary_conninfo so that
the old primary becomes the new standby and points to the new primary.
Synchronous replication
So far, everything that has been set up works in asynchronous mode, PostgreSQL's default behaviour. This section describes the mode that guarantees that no committed transaction is lost, at the price of a tight coupling between the primary and its standby. It is a choice of RPO (Recovery Point Objective: the amount of transactions you accept to lose in case of failure), not of architecture: the setup stays exactly the same, a single parameter changes.
Synchronous replication guarantees that a COMMIT is only acknowledged to the client
once the standby has confirmed that it received and flushed to disk (flush)
the corresponding WAL (with synchronous_commit = on). Depending on the value of this parameter, the confirmation awaited is lighter (remote_write: written to the file
system) or heavier (remote_apply: replayed and visible to reads). It is the safest mode for the RPO (zero data loss).
Enable synchronous replication
One point often misleads: synchronous_commit is already on by default, and yet
replication stays asynchronous. The reason is that synchronous_commit = on only guarantees the local write of the WAL to disk before returning control to the client, as long as no standby is designated. Synchronous mode proper is
enabled by designating at least one standby in synchronous_standby_names,
which is empty by default. It is this parameter, and it alone, that turns sync_state from async
to sync.
Before writing this parameter, you need to know under what name the standby presents itself to the primary. This name is the application_name of its replication connection, and you observe it on the primary:
-- On the primarySELECT application_name, state, sync_state FROM pg_stat_replication;Without an application_name in its primary_conninfo, the standby takes the value of its cluster_name, and walreceiver only if that parameter is empty. Yet a Debian installed from a package does not leave this parameter empty: pg_createcluster writes cluster_name = '18/main' into postgresql.conf. The postgresql.conf of this guide's standby lives in /etc/postgresql/18/main/, which pg_basebackup does not copy: it keeps its own cluster_name, and the column therefore shows 18/main, like on the primary. With a cluster named standby, it showed 18/standby, and walreceiver only came back after emptying cluster_name. That last case is the one of the official Docker image, which explains why many tutorials copy walreceiver.
A tutorial can therefore be right on its author's machine and wrong on yours. The trap is silent: a name that matches no connected standby triggers no error, pg_stat_replication keeps showing async, and every COMMIT waits for nobody. You think you have RPO = 0, you have nothing.
The safest approach is to not depend on this default value: set an explicit application_name in the standby's primary_conninfo. ALTER SYSTEM SET primary_conninfo replaces the whole string, so you must put the host, the role and the password back in (or use ~/.pgpass, see above). A simple reload is enough since PostgreSQL 13: the WAL receiver process reconnects on its own.
-- On the standbyALTER SYSTEM SET primary_conninfo = 'host=192.168.122.61 port=5432 user=replicator password=motdepasse_fort application_name=standby1';SELECT pg_reload_conf();Run the check query again on the primary: the column should now show standby1. Only then, declare this exact name:
-- On the primary-- synchronous_commit = on is already the default value-- It is synchronous_standby_names that enables synchronous modeALTER SYSTEM SET synchronous_standby_names = 'FIRST 1 (standby1)';SELECT pg_reload_conf();| Parameter | Role |
|---|---|
synchronous_commit = on | Default value. synchronous_standby_names empty: the COMMIT waits for the local WAL flush. A named standby: it also waits for that standby to have received and flushed to disk the WAL |
synchronous_standby_names | The real enabler: which standbys are synchronous (by application_name) |
Avoid the wildcard *, which would make any standby that connects synchronous, including an unexpected server. After configuration, pg_stat_replication shows sync instead of async:
SELECT application_name, sync_state FROM pg_stat_replication; application_name | sync_state------------------+------------ standby1 | syncIf the sync_state column stays at async even though the parameter is set, the declared name does not match application_name: reread it character by character, 18/main included.
Trade-offs of synchronous replication
Read this table line by line, as pairs: each advantage has a direct price opposite it. The second line has the heaviest consequences, and it often surprises people in production: the availability of the primary becomes dependent on that of the standby. You have moved the single point of failure, you have not removed it. This is why synchronous replication is generally deployed with at least two standbys.
| Advantage | Drawback |
|---|---|
| RPO = 0 (zero data loss) | Increased latency on every COMMIT |
| Consistency guarantee | If the standby goes down, the primary blocks the COMMITs |
| Confidence for critical data | Requires a reliable and fast network |
Logical replication: an overview
Logical replication is a topic in its own right. Here is a quick overview of the mechanism to understand when to consider it.
Principle
The primary creates a publication on chosen tables. Another PostgreSQL server creates a subscription to receive the changes:
-- On the primary (publisher)CREATE PUBLICATION ma_publication FOR TABLE app.servers, app.deployments;
-- On the subscriber (an independent server)CREATE SUBSCRIPTION ma_souscription CONNECTION 'host=192.168.122.61 dbname=labdb user=replicator password=motdepasse_fort' PUBLICATION ma_publication;When to use logical replication
The first three cases have one thing in common: the source and the destination are two autonomous servers that pursue different goals. The fourth point is a limit to remember before going down this path by mistake, because a subscriber looks like a standby from a distance: it receives data continuously and stays up to date. But it has neither a common timeline with the source, nor any notion of promotion, and nothing guarantees that it contains the entire cluster.
- Version migration: replicate from a PostgreSQL 16 to an 18
- Selective replication: transmit only some tables
- Aggregation: collect data from several sources
- Not for failover: the subscriber is not a standby (no timeline, no promote)
Automatic orchestration tools
The manual failover described above assumes that a human notices the failure, checks the state of the standby and starts the promotion. In practice, this often means several minutes of unavailability, and a much worse risk: split-brain, when the old primary becomes reachable again and accepts writes while the standby has already been promoted. Automating failover is above all about acquiring a consensus mechanism able to designate a single legitimate primary.
Troubleshooting
The standby does not connect
Follow these four checks in order, from the network to the application: each one rules out a layer. Step 3 is the most discriminating, because it reproduces exactly what the
standby does: a connection in replication mode, which uses a different pg_hba.conf line
than an ordinary psql connection. A regular psql that works while replication fails
almost always points to a missing or misplaced replication line.
-
Check network connectivity:
Terminal window # From the standbypg_isready -h 192.168.122.61 -p 5432 -
Check pg_hba.conf on the primary, the replication role must be allowed from the standby's IP:
SELECT user_name, address, auth_methodFROM pg_hba_file_rulesWHERE 'replication' = ANY(database); -
Check the password, test the connection manually:
Terminal window psql "host=192.168.122.61 user=replicator replication=true" -c "IDENTIFY_SYSTEM;"A line with
systemid,timelineandxlogposconfirms that the replication connection succeeds: password andpg_hba.confrule are correct. -
Check the logs of the standby:
Terminal window sudo tail -20 /var/log/postgresql/postgresql-18-main.log
The lag keeps increasing
A lag that keeps growing without ever going back down is not handled like a one-off spike: it means that the standby receives or replays the WAL more slowly than the primary produces it, and the gap will not close by itself. Before going through this table, isolate the faulty layer by comparing the columns of
pg_stat_replication: if sent_lsn is far ahead of flush_lsn, the problem is network or disk
on the standby side; if flush_lsn is close to sent_lsn but replay_lsn lags, the replay is what is blocking, often because of a conflicting read query.
| Possible cause | Solution |
|---|---|
| Very high write load on the primary | Increase wal_keep_size, check the standby's disk IOPS |
| Long queries on the standby (conflicts) | Increase max_standby_streaming_delay or move the queries elsewhere |
| Saturated network | Check the bandwidth between primary and standby |
| Unused replication slot | Check pg_replication_slots, drop the orphan slots |
The promote fails
pg_promote() returns f rather than an error when the promotion does not complete within the allotted time, which gives a poor diagnosis. The three checks below cover the real causes, and the last one is the most rewarding: the server log generally says why the switchover was refused.
- Check that
pg_is_in_recovery()returnstbefore the promote - Check that the user has the rights (superuser, or a role that has been granted
EXECUTEon thepg_promotefunction) - Look at the logs:
sudo tail -30 /var/log/postgresql/postgresql-18-main.log
An orphan slot is filling the disk
The symptom shows up on the primary, not on the standby: pg_wal grows without limit while
the activity has not changed. The query below quantifies the problem before you act, by
translating the LSN gap retained by each inactive slot into a readable volume. Only drop a slot
after confirming that the corresponding standby will not come back: the drop is definitive, and a standby that reconnected afterwards would have to be rebuilt.
-- Identify the inactive slotsSELECT slot_name, active, pg_size_pretty( pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_walFROM pg_replication_slotsWHERE NOT active;
-- Drop an orphan slotSELECT pg_drop_replication_slot('standby_slot');Put it into practice
The lab has you stream a standby on db2.lab from a primary, db1.lab, which holds the facturation database, then prove the data flows. Note that the lab brings up two virtual machines, so dsoxlab provision creates two of them: plan your host's resources accordingly.
The validation first checks the configuration, with this guide's names: a replicator role that can log in and replicate without being a superuser or being able to create databases; a physical slot standby_slot, which must be active, that is, consumed by the standby thanks to primary_slot_name; in the primary's pg_hba.conf, a host or hostssl line for the replication database and an address other than the loopback, whose method is neither trust nor password (the guide uses scram-sha-256); finally a standby in recovery and a streaming state in pg_stat_replication. It does not check the encryption of the stream: host passes, whereas the guide recommends hostssl outside a lab.
The test that matters is the only one that cannot be wrong: it writes a row into the primary's appli.sonde table, laid down empty by the lab, and looks for it on the standby for 20 seconds. A correct configuration that carries nothing does not pass, and the copy method matters little. The last check compares the report /home/student/rapport-replication.txt with the primary: number of streaming standbys, state of the slot, and the fact that replicator is not a superuser.
Key takeaways
- Streaming replication continuously copies the WAL from the primary to the standby; in production, combine it with WAL archiving (
restore_command) to cover prolonged disconnections - Replication is not a backup: a
DROP TABLEis replicated immediately pg_basebackup -Rinitializes the standby and automatically generatesstandby.signal+primary_conninfo- Since PostgreSQL 12,
recovery.confno longer exists, everything goes through the normal parameters - Replication slots prevent premature recycling of the WAL; configure
max_slot_wal_keep_sizeandidle_replication_slot_timeoutto avoid a full disk - The hot standby accepts reads but refuses all writes (
INSERT,UPDATE,DELETE, DDL) pg_promote()turns the standby into an autonomous primary on a new timeline, it is irreversible- Synchronous replication is enabled through
synchronous_standby_names, it guarantees zero data loss but blocks if the standby goes down; the declared name must be the realapplication_name(18/mainorwalreceiverdepending oncluster_name), observed inpg_stat_replication, otherwise nothing is synchronous and no error says so - In production, use an automatic orchestration tool (Patroni, repmgr) for the failover
Knowledge check
Eight questions on what a replication does not protect against, the role of the slot and of primary_slot_name, what makes replication synchronous, the choice between physical and logical replication, the conflicts of the hot standby and the fate of the old primary after a promote.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: MySQL replication: GTID, source-replica, Group Replication and InnoDB Cluster
A new full quiz with random questions
Work again only on the questions you missed