Prove point-in-time recovery from archived WAL
The native PG16.14 lab took a streaming base backup, archived WAL, restored to a timestamp between two commits, kept the before-target row, excluded the after-target row, and promoted in 4.128s on this host.
Problem
What you're actually looking at
The symptom as it shows up on a real server.
A successful backup job is not recovery proof. Point-in-time recovery needs a usable base backup, every required WAL segment, an exact recovery target, and an application-level check that data before the target exists while later data does not.
The lab inserts a base row, takes pg_basebackup with WAL streaming, inserts a keep-before-target row, records a UTC timestamp, inserts an exclude-after-target row, then restores and promotes a separate PostgreSQL instance.
In plain English
PITR restores a base backup and replays archived WAL until a chosen moment. The only useful proof is data: one known row committed before the target must exist, and one committed after it must not. The timer here measures this small local restore, not a production database recovery promise.
Before you start
- • Docker Compose and the repository harness at docker/labs/ha-perf/pitr/run-pitr.sh.
- • A WAL archive on storage independent of the live PGDATA.
- • Enough time and space to restore into a separate data directory.
- • All local passwords and filesystem-copy archive commands are lab-only examples.
How to identify it
- ›pg_stat_archiver has no failed archive commands and archived_count is advancing.
- ›A base backup exists independently of the live data directory.
- ›The recovery target is recorded in UTC with a deliberate commit on each side.
- ›The restored server promotes and contains exactly the expected side of the marker.
Pitfalls to avoid
- ✕Do not call backup completion a restore test. Run the restore into a separate data directory and query application data.
- ✕Do not choose a target without recording timezone and a known transaction on each side.
- ✕Do not delete WAL based only on max_wal_size; archive failures and slots can retain or lose required segments independently.
- ✕Do not call 4.128s a production RTO. The lab has tiny data, local volumes, no object-store latency, and no application bootstrap.
- ✕Do not restore a Patroni member in place while the cluster is active; cluster-level PITR requires a controlled re-bootstrap procedure.
Trace it
Run these against the affected server. To build a copy of this scenario instead, see “Reproduce it in a lab” below.
- 01
Diagnose: verify the restored application marker
Run on the restored instance. The base and before-target rows should exist; exclude-after-target must be absent.
ShellPGPASSWORD=postgres psql -h localhost -p 55471 -U postgres -d labdb -c "SELECT note, created_at FROM recovery_probe ORDER BY id;"Captured after PITRbase-backup-row keep-before-target - 02
Diagnose: verify archiving before attempting recovery
A restore cannot succeed if archive failures are accumulating. Check the source before shutdown and keep the archive independently of PGDATA.
ShellPGPASSWORD=postgres psql -h localhost -p 55470 -U postgres -d labdb -c "SELECT archived_count, failed_count, last_archived_wal, last_failed_wal FROM pg_stat_archiver;"Captured before source shutdownarchived_count > 0 failed_count = 0 last_archived_wal = 000000010000000000000005 last_failed_wal = [NULL]
Resolution approach
- 1.Take a base backup that includes or streams the required WAL.
- 2.Monitor archive success and retain WAL through the recovery window.
- 3.Restore to an isolated data directory with recovery_target_time/LSN/XID and promote.
- 4.Verify application markers and measure the complete service recovery path, not just PostgreSQL readiness.
Stop it recurring
- 01
Keep a repeatable restore assertion
A marker query makes recovery testable and automatable instead of relying on log text.
ShellPGPASSWORD=postgres psql -h localhost -p 55471 -U postgres -d labdb -c "SELECT count(*) FILTER (WHERE note = 'keep-before-target') AS kept, count(*) FILTER (WHERE note = 'exclude-after-target') AS excluded FROM recovery_probe;"Captured live on PostgreSQL 16.14 (docker/labs/ha-perf/pitr)kept | excluded ------+---------- 1 | 0
Reproduce it in a lab
Builds the scenario above on a throwaway database so you can practise the fix. Skip this if you are working a live incident.
- 01
Lab setup (run this first)
Creates a PG16 source, takes pg_basebackup -Xs, archives switched WAL, restores to the timestamp target, and asserts the before row exists while the after row does not.
Shellcd docker/labs/ha-perf/pitr bash run-pitr.shMeasured restore sample · PostgreSQL 16.14recovery_target_time=2026-08-16 01:48:23.805578+00 restore_ready_ms=4128 kept_before_target=1 excluded_after_target=0 promoted=true status=PASS
Verify you're done
SELECT 'Run the PITR harness and require kept=1, excluded=0, promoted=true' AS verification;Last verified 2026-08-16 · PostgreSQL 16.14 (docker/labs/ha-perf/pitr)
Related errors
SQLSTATEs this runbook resolves
The error pages that send an on-call engineer here.
Related runbooks
Continue the same incident path
Sibling procedures that cover the adjacent setup, recovery, or prevention step.
More in this category
Other WAL & replication runbooks
Neighbouring incidents that share the same diagnostic surface.
Connected
How this connects to the rest of the library
A live view of this page's real cross-references — what explains it, what fixes it, what to tune, and where to go next. Every link is an authored relationship, not a guess.
Fixes these errors