Reported symptoms
At 02:38 on 2026-08-19 a release shipped a pricing migration that rewrote the
discount column on historical orders in rbdr-pg-01. Nothing failed. The
deployment was green and the numbers were wrong.
The finance reconciliation finds it on 2026-08-28, nine days later. The request
is narrow: a copy of the database as it stood at 2026-08-19 02:30:00+00, ten
minutes before the deployment.
Everything says this is easy. The nightly base backup has succeeded for eleven
weeks, pg_stat_archiver reports failed=0, the archive is large, and the first
assumption on the call is that this is a matter of typing the timestamp.
Then somebody lists the base backups. The oldest is dated 2026-08-22. An attempt from it reads the target back into the log, begins replay and then dies during startup — and a startup that dies mid-recovery is the shape everyone associates with a missing WAL file, so the second theory is a damaged archive.
Evidence provided
$ ls -1dt --time-style=long-iso /srv/rbdr-base/*/2026-08-28 01:00 /srv/rbdr-base/2026-08-28/
2026-08-27 01:00 /srv/rbdr-base/2026-08-27/
2026-08-26 01:00 /srv/rbdr-base/2026-08-26/
2026-08-25 01:00 /srv/rbdr-base/2026-08-25/
2026-08-24 01:00 /srv/rbdr-base/2026-08-24/
2026-08-23 01:00 /srv/rbdr-base/2026-08-23/
2026-08-22 01:00 /srv/rbdr-base/2026-08-22/Illustrative output
$ /usr/local/bin/rbdr-archive-continuity /srv/rbdr-wal-archivefirst segment : 0000000100000048000000C1 2026-08-22 01:00:09
last segment : 000000010000005B0000002F 2026-08-28 09:12:41
segments present : 4719
segments expected : 4719
gaps : 0
timelines observed : 00000001
timeline history : none present
earliest label file : 0000000100000048000000C1.00000028.backup 2026-08-22 01:00:09Illustrative output
Two facts there do the work: the sequence has no hole, and the archive’s first segment is the one named by the earliest backup label.
$ pg_ctl -D /srv/rbdr-recover -l /srv/rbdr-recover/startup.log start2026-08-28 10:22:41.118 UTC [4417] LOG: starting backup recovery with redo LSN 48/C1000028, checkpoint LSN 48/C1000098, on timeline ID 1
2026-08-28 10:22:41.124 UTC [4417] LOG: restored log file "0000000100000048000000C1" from archive
2026-08-28 10:22:41.126 UTC [4417] LOG: starting point-in-time recovery to 2026-08-19 02:30:00+00
2026-08-28 10:22:41.126 UTC [4417] LOG: redo starts at 48/C1000028
2026-08-28 10:22:41.394 UTC [4417] FATAL: requested recovery stop point is before consistent recovery point
2026-08-28 10:22:41.396 UTC [4411] LOG: startup process (PID 4417) exited with exit code 1
2026-08-28 10:22:41.396 UTC [4411] LOG: aborting startup due to startup process failure
2026-08-28 10:22:41.397 UTC [4411] LOG: database system is shut downIllustrative output
Here is a recovery that did work, measured, for comparison.
$ pg_ctl -D /work/base start 2026-08-28 13:35:12.701 UTC [631] LOG: starting backup recovery with redo LSN 0/3000028, checkpoint LSN 0/3000080, on timeline ID 1
2026-08-28 13:35:12.707 UTC [631] LOG: restored log file "000000010000000000000003" from archive
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: completed backup recovery with redo LSN 0/3000028 and end LSN 0/3000120
2026-08-28 13:35:12.713 UTC [631] LOG: consistent recovery state reached at 0/3000120$ git show 3ac91e7 -- backup/rbdr-base-prune.envDate: Mon Feb 9 09:14:52 2026 +0000
backup: reduce rbdr-base retention 30 -> 7 (Q1 capacity review, 1.9 TiB on rbdr-backup-01)
--- a/backup/rbdr-base-prune.env
+++ b/backup/rbdr-base-prune.env
@@ -2,4 +2,4 @@
-KEEP_BASE_BACKUPS=30
+KEEP_BASE_BACKUPS=7
--- logical data-error incidents, 24 months ---
incidents 14
detected within 7 days 5
detected after 7 days 9
median days to detection 11
95th percentile 31Illustrative output
Work the evidence before reading on
- The archive has no gap, and its first segment is the one named by the earliest backup label. What does that coincidence say about why it begins where it does?
- In the measured recovery, which artefact supplied the LSN that redo started from — the archive, or the base backup?
- The retention change is justified by 1.9 TiB. What number would have had to appear in it for a reviewer to be able to object?
- Nine days is not unusual here. Where is the figure that says so, and who saw it when retention was set?
Root cause
The earliest reachable instant is a property of the oldest base backup
A point-in-time recovery is a base backup replayed forward through archived WAL.
The measured log shows both halves in one line: redo starts at 0/3000028, a
value taken from the base backup, after which segments were fetched in ascending
order.
The earliest reachable instant is therefore the one at which the oldest surviving base backup became consistent: 2026-08-22 01:00. The target is 2026-08-19 02:30, 2 days 22 hours 30 minutes below that floor, with no earlier image for a forward journal to be applied to.
The floor sits there because a prune job keeps seven base backups and expires archive segments older than the oldest one kept. Seven days is shorter than the nine this error spent unnoticed.
The retention number came from the storage budget
Seven was produced by a capacity review. The change states a saving of 1.9 TiB and the budget it serves, no recovery requirement, and no approval from a data owner.
The number that should have set retention was already measured. Nine of fourteen logical data errors in twenty-four months were found more than seven days after introduction, median eleven. Retention shorter than detection latency is a window that exists only for the mistakes somebody notices quickly. The register said so before the change was raised; a storage review never reads it.
Resolution
Stop the floor from moving. The prune runs nightly, and each run takes another base backup and another day of archive with it.
sudo systemctl disable --now rbdr-base-prune.timer
sudo systemctl list-timers --all rbdr-base-prune.timer
Establish the floor as a measurement: the oldest base backup directory, the earliest label file and the first segment name, all three agreeing on one instant. That instant is what the business is told.
Say it early and plainly: the 2026-08-19 02:30 recovery point does not exist here, and no setting creates one. Then hunt an earlier image elsewhere, because nothing else changes the answer — a monthly backup on a different retention, a dump taken for a migration, a volume snapshot on the host.
If nothing earlier exists, change the question. A recovery to the floor still holds the corrupted values, so it restores nothing — but it gives an isolated copy from which the migration’s effect can be characterised and the affected rows identified. Where the migration is deterministic and its inputs survive, recomputation repairs what no recovery could.
Verification
The prune timer is disabled and absent from the timer listing, checked rather than assumed.
The floor is reproduced by a recovery, not read off a listing. A recovery to a
target a few minutes after 2026-08-22 01:00 reaches
consistent recovery state reached, then archive recovery complete, and
accepts connections, with recovery_target_action set explicitly rather than
left at its boot value of pause. Until a recovery has stopped there, the floor
is a claim about filenames.
Archive continuity is recomputed after the retention change: first segment equal to the segment named by the earliest label file, zero gaps to the last.
The age of the oldest base backup is emitted as a metric whose threshold is the 95th percentile of detection latency from the register.
The finding closes when the floor, the requested point and the distance between them have reached the data owner in writing.
Prevention
Derive base backup retention from detection latency. Retention answers how long an error can hide before somebody looks, and the incident register holds that number. Shortening a recovery window to reclaim capacity is the move this scenario is made of: the saving was real, and the recovery point is gone.
Give the figure a data owner and a recorded derivation. A change that alters a recovery window is a change to a recovery objective and needs the same approval. Reviewed as a storage saving, it has no reviewer whose job it is to object.
Publish the earliest recoverable point, and alert on it — one line per protected database, firing when it rises past the detection-latency figure. That number is invisible in job success, archiver counters and repository size.
Keep a long-retention tier the nightly prune cannot reach. A monthly base backup held for a year costs a fraction of the nightly set and moves the floor from weeks to months, the range this class of error occupies.
Rehearse a recovery to a deliberately old target. A restore test that always picks yesterday exercises the newest base backup and says nothing about the floor. Choose a target near the oldest recovery point, quarterly.