PostgreSQLXIII · Backup, Archiving and Point-in-Time RecoveryBackup
Recovery targets and the action taken on reaching one
What you'll learn
- Choose the right recovery target for the situation
- Use recovery_target_inclusive deliberately
- Pick between promote, pause and shutdown
- Recognise when a paused recovery cannot be inspected
Prerequisites
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
The previous lesson used one target and one action. There are five kinds of target and three actions, and the choice among them is usually made under pressure with incomplete information.
The five targets
| Setting | Stops at | Use when |
|---|---|---|
recovery_target_time | A timestamp | You know roughly when |
recovery_target_xid | A transaction id | You know exactly which transaction |
recovery_target_lsn | A WAL position | You know exactly where |
recovery_target_name | A named restore point | You planned ahead |
recovery_target = 'immediate' | Consistency, no further | You want the backup itself |
Only one may be set. Setting two is a startup error, not a precedence rule.
recovery_target = 'immediate' deserves attention: it stops as soon as
the backup is internally consistent and replays no further. That is how
you restore “the base backup, as taken” — useful for verifying a backup
without needing the archive to be complete.
recovery_target_name requires forethought:
-- run this before a risky migration
SELECT pg_create_restore_point('before_v42_migration');
The name is written into WAL. Recovering to it is exact, needs no timestamp arithmetic, and is immune to clock questions. If your change process includes anything irreversible, this one line makes the recovery target unambiguous.
recovery_target_inclusive
The default is on, meaning “stop after the target”. It applies to
recovery_target_time, _xid and _lsn.
The same transaction, two settings, opposite outcomes:
$ recovery_target_xid = '756'
recovery_target_inclusive = onLOG: recovery stopping AFTER commit of transaction 756, time 2026-08-27 21:22:51.345091+00
SELECT count(*) FROM pg_class WHERE relname = 'ledger';
0 <- the DROP was INCLUDED$ recovery_target_time = '2026-08-27 21:22:48.247655+00'LOG: recovery stopping BEFORE commit of transaction 756, time 2026-08-27 21:22:51.345091+00
SELECT count(*) FROM ledger;
1500 <- the table survivedOne word in the log — AFTER or BEFORE — reports which side of the boundary you landed on, and it is the fastest way to confirm the recovery did what you intended before anyone looks at the data.
The three actions
recovery_target_action controls what happens on arrival.
pause (the default)
Stop at the target and hold. The cluster is read-only,
pg_is_in_recovery() stays true, and the server says so:
LOG: pausing at the end of recovery
HINT: Execute pg_wal_replay_resume() to promote.
Measured on 18.6: boot_val is pause, and a recovery that reaches its
target with the setting absent pauses there. It is the right default and
the right choice — it is the only action that lets you look at the data
before the decision becomes permanent.
pause requires hot_standby to be on, which it is by default. With
hot standby off it degrades to shutdown.
promote
End recovery, select a new timeline, open for writes. Correct when you are confident in the target — and it is the behaviour you get without reaching a target at all, because recovery that runs to the end of the archive promotes regardless of this setting.
shutdown
$ pg_ctl -D /restore4 startwaiting for server to start..... done
server shut down because of recovery target settings
LOG: recovery stopping before commit of transaction 756
LOG: shutdown at recovery target
LOG: database system is shut down
$ pg_ctl -D /restore4 status
pg_ctl: no server running
$ ls /restore4/recovery.signal
/restore4/recovery.signal <- STILL PRESENTThe cluster reaches the target and stops without accepting a single
connection. recovery.signal is retained, so the configuration can be
edited and the cluster restarted to try a different target.
This is the right action when the data directory is going to be copied or snapshotted at the target rather than queried, and when you want a guarantee that nothing connects before you have decided.
pause
Recovery stops and the cluster stays in recovery, accepting read-only connections.
$ SELECT pg_is_in_recovery(), pg_get_wal_replay_pause_state();LOG: recovery stopping before commit of transaction 756
HINT: Execute pg_wal_replay_resume() to promote.
pg_is_in_recovery | pg_get_wal_replay_pause_state
-------------------+-------------------------------
t | pausedFrom a pause you can inspect, then decide:
SELECT pg_wal_replay_resume();— continue to the end, or promote if the target was reached.SELECT pg_promote();— end recovery here.- Shut down, change the target, start again.
Choosing a target under pressure
You rarely know the exact instant. A workable order:
- Find the transaction if you can. Application logs, audit tables,
pg_stat_statements. An xid is exact where a timestamp is a guess. - Otherwise bracket the time. Choose a target you are confident is before the damage, even if it loses more than necessary.
- Restore with
pauseorshutdown. Look before promoting. - If you overshot or undershot, do it again with a different target. The failed attempt lives on its own timeline and destroys nothing, as lesson XIII-06 established.
What to take from this
- Five target kinds; only one may be set.
immediaterestores the backup as taken. pg_create_restore_point()before risky work makes the target exact.recovery_target_inclusivedefaults toon— after the target. Measured: the same transaction included or excluded, reported by one word in the log.promoteopens;shutdownstops before any connection;pauseallows inspection.- A paused recovery can hold
AccessExclusiveLockon the object you came to inspect. Measured on the startup process. - A target beyond the end of the archive does not promote silently:
recovery raises
FATAL: recovery ended before configured recovery target was reachedand the cluster does not come up.pg_ctlmay still print “server started”.
Cross-course references
- Linux for Production Sysadmins — Part XLIX (Restore) covers
verifying a restore before it is promoted to being the system of
record, which is exactly what
pauseexists to allow. - Git, CI/CD & GitOps — Part LX (Forward fix versus rollback) covers the same decision in delivery terms: recovering past the mistake is a rollback that also discards everything good after it.
Quiz
Knowledge check · 6 questions
Q1. pg_ctl reports 'server started' for a PITR, but the cluster is not running and the log ends with 'recovery ended before configured recovery target was reached'. What happened?
Q2. Recovery is paused at a target chosen to undo an accidental DROP TABLE. A SELECT against that table hangs. Why?
Q3. A recovery fails with 'requested recovery stop point is before consistent recovery point'. What does this mean and what fixes it?
Q4. Which are valid recovery targets in PostgreSQL 18? Select all that apply.
Q5. Running pg_create_restore_point before a risky migration gives you a recovery target that needs no timestamp arithmetic.
Q6. Compare promote, pause and shutdown as recovery target actions. When is each right?
Passing score: 75%. Answers are checked in this browser.