Reported symptoms
At 09:05 on 2026-08-27 a release ran a migration whose predicate was wrong and rewrote
most of the orders table on the rbdr-orders cluster. The team asks for a
point-in-time recovery to 08:55:00+00, ten minutes earlier.
The recovering instance starts. It restores three segments, reaches a consistent state, accepts read-only connections — and then stops. Its last statement about time is 02:41:07. Nothing says the target was invalid or out of range.
The archive is neither empty nor small: forty-one thousand segments, the newest written at 09:31 that morning. Every check the team runs on it was green.
Evidence provided
Start with the database’s own account of what it handed off.
$ psql -x -c 'SELECT * FROM pg_stat_archiver'-[ RECORD 1 ]------+------------------------------
archived_count | 41892
last_archived_wal | 0000000100000012000000E4
last_archived_time | 2026-08-27 09:31:14.882431+00
failed_count | 1914
last_failed_wal | 0000000100000012000000A3
last_failed_time | 2026-08-27 04:11:52.104663+00
stats_reset | 2026-06-01 00:00:00.117402+00Illustrative output
Then the destination’s account of what exists.
$ ls -1 /srv/rbdr-wal-archive | sort0000000100000012000000A0
0000000100000012000000A1
0000000100000012000000A2
0000000100000012000000B8
0000000100000012000000B9
0000000100000012000000BAIllustrative output
And the recovery log itself.
$ pg_ctl -D /srv/rbdr-pitr start2026-08-27 12:04:18.331 UTC [2214] LOG: starting point-in-time recovery to 2026-08-27 08:55:00+00
2026-08-27 12:04:18.402 UTC [2214] LOG: restored log file "0000000100000012000000A0" from archive
2026-08-27 12:04:18.560 UTC [2214] LOG: consistent recovery state reached at 12/A0000120
2026-08-27 12:04:18.560 UTC [2208] LOG: database system is ready to accept read-only connections
2026-08-27 12:04:18.744 UTC [2214] LOG: restored log file "0000000100000012000000A1" from archive
2026-08-27 12:04:19.011 UTC [2214] LOG: restored log file "0000000100000012000000A2" from archive
cp: cannot stat '/srv/rbdr-wal-archive/0000000100000012000000A3': No such file or directory
2026-08-27 12:04:19.088 UTC [2214] LOG: redo done at 12/A2FFE118
2026-08-27 12:04:19.088 UTC [2214] LOG: last completed transaction was at log time 2026-08-27 02:41:07.556218+00Illustrative output
The contrast is measured: a recovery over a continuous archive on the same major version, captured for this course.
$ psql -Atc 'SELECT archived_count, failed_count, last_archived_wal FROM pg_stat_archiver' pg_stat_archiver:
archived=6 failed=0 last=000000010000000000000005
WAL segments in the archive : 5
000000010000000000000001
000000010000000000000002
000000010000000000000003
000000010000000000000003.00000028.backup
000000010000000000000004
000000010000000000000005$ pg_ctl -D /work/base -o '-p 5434' start 2026-08-28 13:35:12.708 UTC [631] LOG: starting point-in-time recovery to 2026-08-28 13:34:40.077562+00
2026-08-28 13:35:12.708 UTC [631] LOG: redo starts at 0/3000028
2026-08-28 13:35:12.713 UTC [631] LOG: restored log file "000000010000000000000004" from archive
2026-08-28 13:35:12.713 UTC [631] LOG: consistent recovery state reached at 0/3000120
2026-08-28 13:35:12.713 UTC [625] LOG: database system is ready to accept read-only connections
2026-08-28 13:35:12.726 UTC [631] LOG: restored log file "000000010000000000000005" from archive
2026-08-28 13:35:12.735 UTC [631] LOG: recovery stopping before commit of transaction 836, time 2026-08-28 13:34:42.096745+00
2026-08-28 13:35:12.735 UTC [631] LOG: redo done at 0/52EBC90 system usage: CPU: user: 0.01 s, system: 0.00 s, elapsed: 0.02 s
2026-08-28 13:35:12.735 UTC [631] LOG: last completed transaction was at log time 2026-08-28 13:34:38.041366+00The primary’s log shows archive command failures from 02:41 to 04:38, matching a storage
maintenance on the NFS export. The on-call ticket for that night records a
free-space alert on the WAL volume at 03:58, closed at 04:14 by removing segment files
and their archive_status markers.
Work the evidence before reading on
last_archived_walis E4 andlast_failed_walis A3. What does it mean that one is far ahead of the other?- Recovery reached a consistent state and then stopped. Which component does that clear, and which does it leave?
- Compare the two recovery logs. One line present in the measured capture is absent from the incident. Which, and what does its absence assert?
- The archive’s newest file was written at 09:31 and recovery stopped at 02:41. What would have to be true for a wrong target to produce that pair?
- Two monitoring checks existed. Name, for each, what it measured and what it would have had to measure instead.
Root cause
The alternative had to be eliminated first
The obvious reading is that the target was wrong: mistyped, in the wrong timezone, or later than anything the archive holds. Three pieces of evidence eliminate it.
Recovery reached consistent recovery state reached and accepted read-only connections,
so the base backup is sound.
Recovery stopped at 02:41 while the archive’s newest segment was written at 09:31 and
last_archived_wal reads E4. A target beyond the end of the archive drives replay to
the end of the archive; this replay stopped twenty-one files short of it.
And the measured capture shows what reaching a target looks like: recovery stopping before commit of transaction 836, time 2026-08-28 13:34:42.096745+00. Replay saw a
commit later than the target and declined it. The incident log has no such line; it has
redo done immediately after a failed restore, which is what running out of input looks
like.
Twenty-one segments were never written and cannot be recreated
Between 02:41 and 04:38 the NFS export behind /srv/rbdr-wal-archive was offline for a
storage maintenance the database team had not been told about. The archive command
failed on every attempt, which is what failed_count 1914 and last_failed_wal
0000000100000012000000A3 record. The primary carried on serving, because a failure to
archive does not stop a cluster — it stops write-ahead log files being recycled, so
pg_wal grew instead. At 03:58 that raised a free-space alert, closed at 04:14 by
deleting segment files and their archive_status markers. When the export returned at
04:38 archiving resumed at the current segment and never went back, because the markers
that would have told it to were gone.
The two checks that existed could not see any of it
The monitoring repository holds two checks: the nightly base backup’s exit status, and free space in the archive filesystem.
A base backup exits zero whether or not the segments that make it useful reach the
destination, so that status does not mean the cluster is recoverable to a chosen time. The free-space check watched the wrong volume: the archive filesystem was never
short of space, and the one that filled was pg_wal. The single alert this incident
produced fired there, and was closed by the action that made the damage permanent.
Neither failed_count nor the age of the newest archived segment was collected.
Resolution
The recovering instance holds the only assembled copy of the pre-incident data, so it is neither promoted nor overwritten until the reachable window is known.
RBDR_ARCHIVE=/srv/rbdr-wal-archive
ls -1 "$RBDR_ARCHIVE" | grep -E '^[0-9A-F]{24}$' | sort > /tmp/rbdr-segments.txt
wc -l < /tmp/rbdr-segments.txt
The first discontinuity bounds every recovery starting from a base backup older than it. Here the bound is 0000000100000012000000A3, and the last transaction before it committed at 02:41:07. A base backup taken after 04:38 is anchored beyond the hole; the 2026-08-28 one qualifies but postdates the incident, and there is none between 04:38 and 09:05, so 08:55 is unreachable by physical recovery.
The recoverable position is 2026-08-27 02:41:07, six hours fourteen minutes short of what was asked for. That goes to the business as a number of minutes and a number of orders, alongside the 06:00 logical dump if the application can accept a table-level reconstruction. Nobody promotes anything until someone with the authority to accept that loss has accepted it — then a fresh base backup, immediately, because every base backup older than the hole now serves only targets before 02:41.
Verification
A recovery is verified by why it stopped, not by whether the server came up. One that
reached its target logs a recovery stopping before commit line naming a later
transaction; one that ends with redo done straight after a failed restore ran out of
input.
The archive is verified by contiguity:
set -euo pipefail
RBDR_ARCHIVE=/srv/rbdr-wal-archive
prev=""
holes=0
while read -r seg; do
cur=$((16#${seg:8:16}))
if [ -n "$prev" ] && [ "$cur" -ne $((prev + 1)) ]; then
printf 'GAP: %s segment(s) absent before %s\n' "$((cur - prev - 1))" "$seg" >&2
holes=$((holes + 1))
fi
prev="$cur"
done < <(ls -1 "$RBDR_ARCHIVE" | grep -E '^[0-9A-F]{24}$' | sort)
[ "$holes" -eq 0 ] || exit 1
The archiver is verified by two readings ten minutes apart: failed_count unchanged,
and last_archived_wal advanced or the newest archived file younger than
archive_timeout plus a margin.
The recovered data is verified against a property recorded before the incident: the row count and the sum of the amount column for orders written before 02:41:07, against the pre-release report. A recovery to the wrong point matches neither figure.
Prevention
Alert on the archiver. The delta in pg_stat_archiver.failed_count over an
interval, and the age of the newest file in the archive. Either would have raised a
ticket at 02:45.
Audit continuity on a schedule: size and free space say nothing about holes.
Never reclaim space on a WAL volume by removing segments or their archive_status
markers. Fix the destination, extend the volume, or accept the cluster stopping — all
three are reversible; that one is not.
Make the archive command fail loudly, and take a base backup after every archiving interruption, because a hole retires every earlier base backup for every target after it.