PITR fails after a silent gap in the WAL archive
A 2 TB OLTP database takes a nightly pg_basebackup at 02:00 to an NFS mount, with archive_mode = on and archive_command = '/usr/local/bin/archive_wal.sh %p %f'. The script copies the segment; its final line is exit 0, added by a former DBA so that the logs would stop filling with errors. At 14:32 an operator ran DELETE FROM orders; with no WHERE clause and committed. A restore on a staging host replaying to 14:30 stops with: could not find file 0000000100000A3F0000007B in the archive. pg_stat_archiver on production shows failed_count = 0 and a last_archived_time only a few minutes old. There is one physical standby on the same network, streaming normally.
1. Explain what happened and why the existing monitoring did not catch it.
Read the worked answer
archive_command is a contract: exit status 0 means the file is durably stored, and PostgreSQL is then free to recycle or remove that segment from pg_wal. The wrapper script returns 0 unconditionally, so a transient NFS error on one segment was reported as success. That segment was recycled, and the archive now has a permanent, unrecoverable gap.
Because each command call succeeded from PostgreSQL's point of view, pg_stat_archiver.failed_count stayed at 0 and last_archived_time kept advancing - the two metrics most teams alert on. The failure is invisible from inside the server.
The correct idiom never lies about failure:
test ! -f /mnt/archive/%f && cp %p /mnt/archive/%f
If the copy fails or the file already exists, the command returns nonzero, PostgreSQL retries the same segment forever and retains WAL - which grows pg_wal, an alertable symptom, instead of creating a hole.
Monitoring that would have caught it:
- Alert on
pg_stat_archiver.last_failed_time/failed_countand on staleness oflast_archived_time(no successful archive in N minutes). - Independently verify continuity from outside: compare the archive listing against
pg_walfile_name(pg_current_wal_lsn())and assert that segments form an unbroken sequence. - Use a tool that tracks and verifies what it shipped (pgBackRest, WAL-G) rather than a hand-written cp wrapper.
2. What is the real recovery point objective now, and what are your options for getting back to 14:31?
Read the worked answer
The RPO is not 'the last backup' - it is 'the last point before the gap'. Continuous recovery is possible only from the 02:00 base backup forward through uninterrupted WAL. Everything after 0A3F/7B is unusable because replay cannot skip a segment. So the theoretical best recovery point is wherever the archive broke, which could be hours or days earlier; nothing after it can be reached.
Options, in order of value:
- Find the missing segment. It is a single 16 MB file. Check:
- the streaming standby's
pg_wal- it received that WAL and may still hold it; - the primary's own
pg_walifwal_keep_sizeretained it and it has not been recycled; - any NFS snapshots of the archive mount (verify contents against the file's expected size and checksum, not just filename).
- the streaming standby's
- If no copy exists, run a second restore that stops at the last segment before the gap and accept the data loss, then reconcile with application-level records (order confirmations, downstream systems) to quantify what was lost.
- Only then decide whether to also replay forward by re-applying the archive from whatever point is reachable - do not attempt to stitch files of unknown provenance into the archive; a corrupted segment produces a corrupt database silently.
Do not use recovery_target_time = '14:31' when the WAL does not exist; choose the target only after confirming continuity with pg_waldump on the candidate segments. And restore to a separate host, never over the primary - the deleted rows may be recoverable from the primary's own heap before it is vacuumed anyway.
3. Design the fixed archiving setup and the recovery drill that would have prevented this.
Read the worked answer
Archiving
- Replace the wrapper with the
test ! -f ... && cp ...form, or move to pgBackRest/WAL-G with repository verification and retention. - Alert on three independent signals:
pg_stat_archiver.failed_countincreasing,last_archived_timeolder than ~5 minutes, and pg_wal file count / total size above a threshold (when an archive stalls, pg_wal grows - that is the early warning you want). - Set
archive_mode = alwayson the standby too, so a standby being promoted does not suddenly stop archiving. - Keep the archive immutable: no process other than PostgreSQL's archiver writes there, and it is not the same filesystem as pg_wal.
Drill, scheduled monthly
- Provision a scratch host, unzip the latest base backup, set
restore_commandand arecovery_target_timefrom a log line in the drill scenario. - Set
recovery_target_action = 'pause'(not the defaultpromote) so you can inspect the database before it opens for writes, thenpg_wal_replay_resume()or cancel after checking row counts. - Validate the backup itself with
pg_verifybackupbefore any restore attempt. - Record actual RTO from a cold start, and compare the achieved recovery point against the target - a backup you have never restored is an assumption, not a backup.
- Rotate the scenario: full restore, single-table restore, restore to a point in the middle of the retained window.