PostgreSQLXIII · Backup, Archiving and Point-in-Time RecoveryBackup
Point-in-time recovery, performed
What you'll learn
- Perform a point-in-time recovery end to end
- Read a recovery log and identify the lines that report the outcome
- Distinguish real restore_command failures from normal probing
- Verify a recovery before anyone depends on it
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
Everything in this part has been building to one operation. This lesson is a recovery that was actually performed, transcribed.
The disaster
$ # the sequence that was run INSERT 1000 rows 'baseline row N'
[base backup taken here]
INSERT 500 rows 'good row N'
T1 = 2026-08-27 21:22:48.247655+00 <- 1500 rows present
DROP TABLE ledger; <- THE DISASTER
T2 = 2026-08-27 21:22:51.397478+00
CREATE TABLE aftermath; INSERT ... <- work done after the disaster
SELECT pg_switch_wal(); <- so the last segment is archivedThree seconds separate the good state from the disaster. The goal is to recover to T1: every row that existed then, nothing created after.
The restore
$ tar xzf /backup/base/base.tar.gz -C /restore
mkdir -p /restore/pg_wal && tar xzf /backup/base/pg_wal.tar.gz -C /restore/pg_wal
chmod 700 /restore# no output; the mode 700 matters, PostgreSQL refuses to start otherwise$ cat >> /restore/postgresql.auto.conf <<EOF
restore_command = 'cp /archive/%f %p'
recovery_target_time = '2026-08-27 21:22:48.247655+00'
recovery_target_action = 'promote'
archive_mode = 'off'
port = 5433
EOF
touch /restore/recovery.signal# no outputEvery line has a reason:
| Setting | Why |
|---|---|
restore_command | How recovery fetches segments. The inverse of archive_command. |
recovery_target_time | Where to stop. T1, the instant before the DROP. |
recovery_target_action | What to do on arrival. Lesson XIII-07. |
archive_mode = 'off' | So the copy does not archive into the original’s location. |
port = 5433 | So it does not fight the original for a port. |
The recovery log, verbatim
$ pg_ctl -D /restore start && tail -f /restore/log/postgresql.logLOG: starting PostgreSQL 18.6 ...
LOG: listening on IPv4 address "0.0.0.0", port 5433
LOG: database system was interrupted; last known up at 2026-08-27 21:22:32 UTC
cp: cannot stat '/archive/00000002.history': No such file or directory
LOG: starting backup recovery with redo LSN 0/3000028, checkpoint LSN 0/3000080, on timeline ID 1
LOG: restored log file "000000010000000000000003" from archive
LOG: starting point-in-time recovery to 2026-08-27 21:22:48.247655+00
LOG: redo starts at 0/3000028
LOG: restored log file "000000010000000000000004" from archive
LOG: completed backup recovery with redo LSN 0/3000028 and end LSN 0/3000120
LOG: consistent recovery state reached at 0/3000120
LOG: database system is ready to accept read-only connections
cp: cannot stat '/archive/000000010000000000000005': No such file or directory
LOG: recovery stopping before commit of transaction 756, time 2026-08-27 21:22:51.345091+00
LOG: redo done at 0/4034B48 system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.01 s
LOG: last completed transaction was at log time 2026-08-27 21:22:46.193856+00
LOG: restored log file "000000010000000000000004" from archive
cp: cannot stat '/archive/00000002.history': No such file or directory
LOG: selected new timeline ID: 2
cp: cannot stat '/archive/00000001.history': No such file or directory
LOG: archive recovery complete
LOG: checkpoint starting: end-of-recovery immediate wait
LOG: checkpoint complete: ... distance=16594 kB
LOG: database system is ready to accept connectionsThe lines that look like failures and are not
cp: cannot stat '/archive/00000002.history': No such file or directory
cp: cannot stat '/archive/00000001.history': No such file or directory
Recovery probes for timeline history files. These are the
restore_command reporting a miss, and a miss is exactly how recovery
discovers there is no later timeline to follow. On a successful first
recovery they are guaranteed to appear.
cp: cannot stat '/archive/000000010000000000000005': No such file or directory
Recovery asks for the next segment, is told there is none, and concludes the archive ends there. On a PITR whose target is comfortably before the end of the archive this line does not appear; here it did because the target was near the end.
Three alarming-looking lines in a recovery that worked perfectly. An operator who aborts on seeing them abandons a correct recovery.
The lines that report the actual outcome
LOG: recovery stopping before commit of transaction 756, time 2026-08-27 21:22:51.345091+00
LOG: last completed transaction was at log time 2026-08-27 21:22:46.193856+00
LOG: selected new timeline ID: 2
The first names the transaction recovery stopped before — the DROP.
The second reports what actually made it in. The third records the
promotion.
Note the gap: the target was 21:22:48.247655 and the last completed transaction was at 21:22:46.193856, two seconds earlier. Recovery stops before the first commit at or after the target, so the true stopping point is the last commit that precedes it. You do not get the target instant; you get the last consistent state before it.
Verifying
$ SELECT min(id), max(id),
count(*) FILTER (WHERE note LIKE 'good%') AS good_rows,
count(*) FILTER (WHERE note LIKE 'baseline%') AS baseline_rows
FROM ledger;
SELECT count(*) FROM pg_class WHERE relname = 'aftermath';
SELECT pg_is_in_recovery(); min | max | good_rows | baseline_rows
-----+------+-----------+---------------
1 | 1500 | 500 | 1000
count
-------
0 <- work done AFTER the target is absent
pg_is_in_recovery
-------------------
f <- promoted, accepting writesAll 1,500 rows. The dropped table is back, the work done after the disaster is absent, and the cluster is promoted.
$ pg_controldata /restore | grep TimeLineID
cat /restore/pg_wal/00000002.historyLatest checkpoint's TimeLineID: 2
Latest checkpoint's PrevTimeLineID: 1
1 0/4034B48 before 2026-08-27 21:22:51.345091+00One line, three fields: the timeline diverged from timeline 1, at LSN 0/4034B48, because it was told to stop before 21:22:51.345091+00. Every future recovery reads this file to know that timeline 2 exists and where it branched.
Meanwhile, on the original:
$ SELECT count(*) FROM pg_class WHERE relname IN ('ledger','aftermath'); count
-------
1 <- only aftermath; ledger is still droppedBoth states exist. Nothing was destroyed to produce the recovery, and the operator can now compare the two and decide what to do — copy the table back, switch traffic to the restored cluster, or try a different target.
What to take from this
- Recover onto a copy, on a different port, with
archive_mode = off. recovery.signalis what makes recovery happen. Without it every recovery setting is silently ignored.cp: cannot stat '...history'is normal probing, not a failure.- Recovery stops before the first commit at or after the target, so the true stopping point is the last commit preceding it. Measured: a two-second gap.
- Verify by content, not by “it started” — row counts, absent objects,
pg_is_in_recovery(). - Promotion creates a new timeline, and the 40-byte
.historyfile is what makes both histories navigable.
Cross-course references
- Linux for Production Sysadmins — Part XLIX (Restore) and Part L (Disaster recovery) cover rehearsing a recovery rather than documenting one, and Part LXXXI (Incident command) covers who chooses the recovery target under pressure.
- Observability for Production Sysadmins — Part CIX (Incident investigation workflows) covers establishing when the damage happened, which is the input this whole procedure depends on.
Quiz
Knowledge check · 6 questions
Q1. An operator restores a base backup, appends restore_command and recovery_target_time to postgresql.auto.conf, and starts the cluster. It comes up cleanly but contains only the data present at the moment of the base backup. What was omitted?
Q2. During a PITR the log shows "cp: cannot stat '/archive/00000002.history': No such file or directory". What does this mean?
Q3. A recovery targeted 21:22:48.247655 and the log reports the last completed transaction was at 21:22:46.193856. Why the two-second gap?
Q4. Which settings belong in the recovery configuration of a PITR performed beside a still-running original cluster? Select all that apply.
Q5. Because a failed PITR attempt lives on its own timeline, the same base backup can be restored again with a different target without having destroyed anything.
Q6. Why does promotion create a new timeline, and what breaks if a .history file is lost?
Passing score: 75%. Answers are checked in this browser.