Runbook: Rejoin a Former Primary With pg_rewind
1 · Prerequisites
Confirm every item is in place before any state change.
- A new primary that is healthy and is the agreed survivor
- The old primary, stopped or stoppable, whose data directory is intact
- Confirmation that full_page_writes was on at the source, and either a superuser connection to it or a role holding the four file-access function grants pg_rewind requires
- The WAL from the divergence point onwards, still present on the new primary or in the archive
- Storage for a copy of the old primary data directory, or at least a pg_dump of the tables that may have diverged
- A decision, recorded and owned, that the new primary is the survivor and the old primary work is being discarded
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · Establish whether the two nodes actually diverged. On each:
SELECT pg_is_in_recovery(), timeline_id FROM pg_control_checkpoint();and read the.historyfiles inpg_wal. If the old primary accepted no writes after the promotion, it may be able to follow the new primary without a rewind at all. - · Enumerate the divergence before touching anything. Compare row counts and the latest timestamps on the tables the old primary may have written. This is evidence-gathering and it cannot be repeated after the rewind.
- · Take a copy. Either the whole data directory or a
pg_dumpof the affected tables. Oncepg_rewindcompletes, the divergent rows exist nowhere, and reconciling by hand is frequently the right answer for a small number of them. - · **Confirm
full_page_writeswas on at the source.**SHOW full_page_writes;on the new primary.pg_rewindrequires it, because it reconstructs pages from WAL. - · Confirm the WAL from the divergence point still exists.
pg_rewindneeds to read forward from the last common checkpoint. If segments have been recycled everywhere, it fails and the node must be rebuilt from a base backup instead. - · Confirm the target is cleanly shut down.
pg_rewindrefuses to run against a target that was not shut down cleanly, and the fix is to start it, let it recover, and stop it cleanly — not to force anything. - · **Confirm the connection
pg_rewindwill use.** It needs either a superuser, or a role grantedpg_read_binary_file,pg_read_file,pg_stat_fileandpg_ls_dir. A missing grant producespermission denied for function pg_read_binary_filepartway through.
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Stop the old primary cleanly if it is running.
pg_ctl -D <datadir> -m fast stop. If it was killed rather than stopped, start it, let it complete crash recovery, then stop it cleanly.pg_rewindwill not proceed otherwise, and that refusal is protecting you. - 2Take the copy now, before anything else.
cp -a <datadir> <datadir>.pre-rewindor apg_dumpof the affected tables from a temporary start of the old primary. This is the last moment at which the divergent work exists. - 3Run a checkpoint on the new primary.
CHECKPOINT;This makes the common ancestor easier forpg_rewindto find, and reduces the amount of WAL it must read. - 4**Run
pg_rewindin dry-run mode first.**pg_rewind --target-pgdata=<datadir> --source-server='host=new-primary user=repl' --dry-run -P. It reports the divergence point without modifying anything. - 5Read what it reports.
pg_rewind: servers diverged at WAL location 0/43CCB2E0 on timeline 1andpg_rewind: rewinding from last common checkpoint at 0/43C807B8 on timeline 1. The gap between those two LSNs is what will be undone. - 6Run it for real.
pg_rewind --target-pgdata=<datadir> --source-server='host=new-primary user=repl' -P. - 7Write the standby configuration.
pg_rewinddoes not do this: setprimary_conninfopointing at the new primary with a meaningfulapplication_name, andprimary_slot_nameif you are using a slot. Create the slot on the new primary first. - 8**Create
standby.signal** in the target data directory. Without it the node starts as a primary, which is exactly the situation you are recovering from. - 9**Check
postgresql.auto.conffor stale settings.** The old primary's file may still carrysynchronous_standby_names,archive_modeand other settings that belong to a primary and do not belong on a standby. - 10Start it and read the log. Expect it to begin recovery on the old timeline, replay forward through the branch point, and then stream on the new timeline:
starting backup recovery with redo LSN ... on timeline ID 1followed bystarted streaming WAL from primary at ... on timeline 2. - 11Confirm it is a standby.
SELECT pg_is_in_recovery();must returntrue, andpg_stat_replicationon the new primary must show it streaming. - 12Confirm the divergent rows are gone, and record it. The rejoined node now matches the new primary exactly. Whatever it wrote alone has been discarded — this is not a defect, it is what rejoining a diverged node means, and somebody outside the database team needs the count.
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓
pg_rewindexited zero and its output named a divergence point consistent with the promotion time. - ✓
SELECT pg_is_in_recovery();returnstrueon the rejoined node. - ✓
pg_stat_replicationon the new primary shows it withstate = 'streaming'and a shrinking byte lag. - ✓A row written on the new primary appears on the rejoined node within seconds.
- ✓Row counts on the affected tables match between the two nodes exactly.
- ✓The rejoined node's
postgresql.auto.confcontains no settings belonging to a primary —synchronous_standby_names, andarchive_modeif the estate archives from one node only. - ✓Exactly one node in the estate reports
pg_is_in_recovery()asfalse. - ✓The discarded work has been enumerated and recorded, with the pre-rewind copy retained until somebody has decided what to do about it.
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶Before
pg_rewindruns, rollback is to leave the node alone. Nothing has been modified. - ↶After
pg_rewindruns, there is no rollback within the data directory — that is what the pre-rewind copy is for. Restore it if you need the divergent state back, and understand that the node then cannot rejoin without repeating the rewind. - ↶If
pg_rewindfails partway, the target data directory may be in an inconsistent state. Do not start it. Restore the pre-rewind copy, or rebuild from a base backup of the new primary. - ↶If the node cannot be rejoined at all, rebuild it with
pg_basebackupfrom the new primary. That is slower and it is not a failure of this procedure. - ↶If the rejoined node was started without
standby.signaland came up as a primary, stop it immediately and check whether anything wrote to it. A second primary that has accepted writes is a new split brain layered on the old one. - ↶Retain the pre-rewind copy until the reconciliation of the discarded work is complete and signed off, then remove it deliberately with a note.
6 · Escalation
When the runbook isn't enough, contact:
- ·
pg_rewindreports it cannot find a common ancestor, or that the required WAL is missing: escalate and rebuild from a base backup. This is a normal outcome when segments have been recycled and it is not worth fighting. - ·
pg_rewindfails withpermission denied for function pg_read_binary_file: escalate to whoever manages the replication role, or use a superuser connection. The four required grants arepg_read_binary_file,pg_read_file,pg_stat_fileandpg_ls_dir. - · The divergent work is material — orders, payments, anything a customer can see: escalate to the data owner before running the rewind. The decision to discard committed, acknowledged transactions is not a database-team decision.
- · The target will not shut down cleanly and cannot be started to recover: escalate. Forcing
pg_rewindpast that check is not available, and it is not available deliberately. - · It is not clear which node should be the survivor: escalate rather than choosing. Usually the node with the larger divergence and the longer period of live traffic wins, but that is an argument to be made and recorded, not assumed.
- · The rejoined node has different hardware or configuration and cannot keep up with the primary's write rate: escalate to the platform owner. A standby that falls permanently behind is not a failover target.
pg_rewind exists to avoid a full base backup when a former primary
needs to become a standby. It does that by rewinding the node to the last
checkpoint the two share, then replaying forward along the winning
timeline.
Which means the node’s divergence — everything it wrote after the split — is discarded. That is not a side effect. It is the operation.
Enumerate and preserve before you run it
What it needs
| Requirement | If missing |
|---|---|
| Target cleanly shut down | pg_rewind refuses — start it, let it recover, stop it cleanly |
full_page_writes on at the source | pg_rewind refuses; it reconstructs pages from WAL |
| WAL from the divergence point still present | pg_rewind fails; rebuild from a base backup instead |
| Superuser, or four function grants | permission denied for function pg_read_binary_file, partway through |
The four grants, if you are not using a superuser:
GRANT EXECUTE ON FUNCTION pg_read_binary_file(text) TO repl;
GRANT EXECUTE ON FUNCTION pg_read_file(text) TO repl;
GRANT EXECUTE ON FUNCTION pg_stat_file(text) TO repl;
GRANT EXECUTE ON FUNCTION pg_ls_dir(text) TO repl;
What it tells you
$ pg_rewind --target-pgdata=/var/lib/postgresql/18/main --source-server='host=new-primary user=repl' -Ppg_rewind: servers diverged at WAL location 0/43CCB2E0 on timeline 1
pg_rewind: rewinding from last common checkpoint at 0/43C807B8 on timeline 1Everything between those two LSNs on the losing timeline is what is being
undone. Run with --dry-run first and read those two lines before
committing to it.
pg_rewind does not write the standby configuration
It leaves you with a data directory that matches the new primary’s
history. It does not create standby.signal, and it does not set
primary_conninfo.
Start it without those and it comes up as a primary — which is precisely the situation you are recovering from.
touch /var/lib/postgresql/18/main/standby.signal
And check postgresql.auto.conf for settings that belonged to a primary:
synchronous_standby_names, archive_mode, anything else that was set
while this node led.
What a successful rejoin looks like
$ tail -3 postgresql.logLOG: starting backup recovery with redo LSN 0/43C80728, checkpoint LSN 0/43C807B8, on timeline ID 1
LOG: started streaming WAL from primary at 0/43000000 on timeline 2
LOG: database system is ready to accept read-only connectionsIt replays forward through the branch point and then follows the new timeline. That pair of lines — timeline 1 then timeline 2 — is the signature of a correct rejoin.
Blast radius
| Action | Reversible? | What it costs if wrong |
|---|---|---|
--dry-run | Yes | Nothing |
| Taking the pre-rewind copy | Yes | Disk, briefly |
pg_rewind for real | Only from the copy | Every transaction the node committed alone |
A failed pg_rewind partway | No | An inconsistent directory — restore the copy or rebuild |
Starting without standby.signal | No, if anything writes | A second primary, and a new split brain |