Production reliability checklists

How to test PostgreSQL point-in-time recovery before an incident

Prove one frozen PostgreSQL recovery path in isolation: verify its inputs, recover to an explicit target, check application postconditions, measure the complete path, and prove cleanup.

Short answer

To test PostgreSQL point-in-time recovery before an incident, restore one immutable base backup and its required archived WAL into a disposable environment that cannot reach production clients or external-effect systems. Record the PostgreSQL version, backup identity, manifest, base-backup end time, WAL range, archive configuration, encryption-key references, extensions, tablespaces, locale, and recovery objective. Verify the selected inputs, but do not treat a successful pg_verifybackup run as proof that PostgreSQL can start or that application data will be correct.

Choose a named restore point or an unambiguous timestamp after the base backup ended. Configure the intended archive-recovery path and recovery.signal, capture every requested WAL segment and recovery log, and preserve the resulting timeline. Keep the recovered cluster isolated after startup while checking PostgreSQL integrity, roles, ownership, extensions, sequences, row-level security, and application-specific records around the target. Measure retrieval, replay, startup, validation, and handoff separately. Close only after the observed recovered point and end-to-end time are compared honestly with the stated objectives and all restored data, credentials, snapshots, and endpoints are cleaned up or retained under an explicit policy.

The operating rule is:

Prove one frozen recovery path in isolation, including application postconditions and cleanup; do not infer recoverability from backup success or manifest verification alone.

1. Freeze one testable recovery contract

A general statement such as “we retain seven days of WAL” is not a recovery test. Bind the rehearsal to one exact input set and one declared objective:

rehearsal_id:
postgresql_server_major_and_minor:
restore_binary_version:
source_system_identity:
base_backup_identity:
base_backup_started_at:
base_backup_ended_at:
backup_manifest_digest:
backup_format_and_compression:
wal_archive_identity_and_range:
archive_configuration_version:
encryption_key_references:
extensions_and_versions:
tablespaces_and_paths:
locale_and_collation:
recovery_target_type: name | time | lsn | xid
recovery_target_value:
recovery_target_timezone:
recovery_target_inclusive:
recovery_point_objective_under_test:
recovery_time_objective_under_test:
rehearsal_owner:
validation_owner:

PostgreSQL point-in-time recovery uses a base backup plus a continuous sequence of archived write-ahead log records. The target must be reachable from that selected base backup. PostgreSQL's current continuous-archiving documentation states that a recovery target must be after the base backup's ending time. Record that boundary before attempting recovery; do not keep trying earlier timestamps until one happens to start.

Freeze the manifest and input identities outside the writable restore destination. Include checksums or immutable object versions where the storage system supplies them. A bucket prefix, “latest” pointer, mutable snapshot name, or current symlink is not a stable cohort. If retention can delete WAL during the rehearsal, copy or protect the exact required range under a bounded test policy.

Name the archive command, restore command, compression and encryption path, credential source, tablespace mapping, and configuration transformations that the incident runbook expects to use. A recovery that succeeds only through undocumented manual edits tests an improvised path rather than the runbook.

2. Build an isolated destination before touching the backup

The recovered database can contain production-like credentials, schedules, webhooks, notification queues, payment intents, replication settings, and personal data. Isolation is therefore an effect-control requirement, not merely a convenience.

Define the destination boundary explicitly:

restore_host_or_cluster:
network_egress_policy:
allowed_operator_ingress:
production_dns_resolution:
production_client_route:
replication_destinations:
job_scheduler_state:
email_and_notification_route:
payment_and_webhook_route:
message_broker_route:
secret_source:
restored_data_handling_class:
retention_deadline:
cleanup_owner:

Deny routes to production applications, queues, webhooks, mail systems, payment providers, schedulers, and replication peers before PostgreSQL starts. Use test-only credentials and destinations wherever startup checks require dependencies. Do not rely on “nobody knows the host name” or on an application flag that is loaded only after background jobs begin.

Capacity is part of the destination contract. Verify free space for the expanded backup, WAL staging, temporary files, logs, and validation work. Verify memory, file-descriptor, shared-memory, ownership, and permission requirements. Record required extension packages, server binaries, locale data, certificates, key access, and tablespace paths. A file-valid backup can still fail because the restore host lacks one of these dependencies.

Do not mount the only retained backup read-write. Keep the selected source immutable and restore into a fresh destination. If the rehearsal needs transformed configuration or remapped paths, preserve the transformation as a versioned artifact and test it again on later rehearsals.

3. Verify inputs without overstating what verification proves

For a supported plain-format base backup with a manifest, run pg_verifybackup using the matching PostgreSQL tooling and retain its command, version, output, and exit status. Verify the manifest digest before and after transfer. Check archive object readability, decryption, decompression, ownership, required WAL coverage, timeline-history files, and storage retention.

A useful input gate records:

manifest_present_and_digest_matches:
pg_verifybackup_command_and_version:
pg_verifybackup_result:
wal_parse_or_coverage_result:
archive_objects_readable:
decryption_tested:
required_keys_available:
required_timeline_history_present:
backup_and_wal_source_immutable:
restore_space_headroom:
server_binary_compatible:
extensions_available:
tablespaces_mapped:

PostgreSQL explicitly cautions that pg_verifybackup cannot perform every check that a running server performs and that test restores are still required. A passing result supports the narrower claim that the files examined agree with the manifest checks the utility performed. It does not establish startup compatibility, WAL completeness for the selected target, logical consistency, correct roles, application behavior, or recovery-time performance.

Treat a failed input check as evidence to repair the backup or archive process. Do not delete unexpected files, substitute a different WAL segment, disable checksum checks, or edit the manifest merely to make the test continue. Preserve the failure, identify whether the source, transfer, retention, encryption, or destination caused it, and restart with a newly declared cohort when necessary.

If the backup lacks a manifest, state that limitation rather than generating one after the fact and presenting it as contemporaneous backup evidence. Use all other available storage checks, then prioritize replacing the backup process with one that emits verifiable identities.

4. Select an explicit and auditable recovery target

Choose the target from an event that can be checked after recovery. A named restore point is often easier to identify than a wall-clock time, but either requires an application-specific reason and an expected data boundary.

For a time target, record the full timestamp, timezone, and whether the target is inclusive. For a named target, retain evidence of when and on which source timeline it was created. For any target, identify records or state transitions expected on both sides:

target_business_event:
known_committed_before_target:
known_committed_after_target:
expected_present_records:
expected_absent_records:
expected_sequence_or_counter_bounds:
source_timeline:
expected_recovery_timeline_behavior:

Do not use personal data in a broadly shared rehearsal record. Prefer synthetic canary rows inserted through an approved process, privacy-safe stable record identifiers, aggregate invariants, and restricted evidence links.

Configure recovery using the PostgreSQL version's documented parameters and create recovery.signal for archive recovery. Review generated settings rather than copying a standby configuration blindly. standby.signal, primary connection settings, or pg_basebackup --write-recovery-conf can produce behavior intended for a standby; they should not silently replace a bounded archive-recovery test.

Set the recovery action deliberately. Know whether the server should pause, shut down, or promote when the target is reached, and how that choice affects validation and timeline creation. Preserve the original configuration plus the exact rendered test configuration. Never point the restore command at a mutable location without recording which objects it returned.

5. Exercise the intended restore path and capture evidence

Start the restored cluster through the same automation, service manager, container image, or runbook path intended for an incident. Begin timing before backup retrieval if retrieval is part of the recovery objective. Capture phases separately:

backup_discovery_started:
backup_retrieval_started_and_finished:
wal_retrieval_started_and_finished:
restore_preparation_started_and_finished:
postgres_start_requested:
recovery_target_reached:
cluster_available_for_validation:
application_validation_finished:
handoff_ready:

Capture PostgreSQL logs, each missing or requested WAL identity, archive-restore errors, target-reached evidence, promotion or pause behavior, timeline ID, and timeline-history artifact. Do not edit around unexplained failures and then report only the successful final start. Record every intervention and decide whether it belongs in the runbook or invalidates this trial.

Watch for a restore command that retries forever on a segment that cannot exist, silently reads from the wrong archive, decrypts with an obsolete key, or follows an unintended timeline. Bound retries and define stop conditions. A test that depends on an operator noticing a quiet stall does not provide a reliable recovery path.

When PostgreSQL starts, keep the destination isolated. “Accepting connections” is one infrastructure checkpoint, not recovery completion. Confirm that the instance is the restored destination, has the expected system identity, stopped at the intended boundary, and is not connected as a production standby or scheduler target.

Preserve logs in a restricted location with retention and privacy controls. Logs can contain SQL fragments, object names, paths, credentials from faulty commands, and business identifiers. Redact only the reporting copy; preserve access-controlled originals when incident policy requires them.

6. Verify database and application postconditions

Validation needs both PostgreSQL-level and application-level evidence. Start with the physical and catalog boundary:

Then test application invariants around the target. Use read-only checks unless the rehearsal explicitly includes isolated write validation. Confirm that known pre-target records and relationships are present, known post-target events are absent, balances or counts reconcile under the application's rules, and authorization state matches the intended moment. A row-count comparison alone can miss duplicated, missing, or semantically inconsistent records.

Record each assertion with its evidence source:

check_id:
layer: postgresql | application | effect-isolation
query_or_method_version:
expected_result:
observed_result:
evidence_location:
reviewer:
status: pass | fail | unknown | not_applicable

Unknown is not a pass. If a known pre-target transaction is absent or a post-target transaction is present, first verify target interpretation, timezone, inclusivity, source timeline, and evidence identity. Do not move the target until the output looks convenient. Explain whether the recovery configuration, expected record, or source event boundary was wrong.

Do not claim application recovery if the database passes catalog checks but the application cannot start with the restored roles, extensions, sequences, feature configuration, or security policies. Conversely, a landing page that loads is not broad database verification. Select invariants from the recovery contract's most important obligations.

7. Measure the complete objective honestly

Separate the recovery-point objective from the recovery-time objective. The recovered point is the amount of source history represented by the validated target. The recovery time is the complete elapsed path required to make the isolated recovered service ready for the declared handoff—not only WAL replay or PostgreSQL startup.

Report phase timings and exclusions:

backup_selection_and_authorization:
backup_retrieval_and_decryption:
wal_discovery_transfer_and_replay:
destination_preparation:
postgresql_start_and_target_handling:
database_validation:
application_validation:
network_or_handoff_preparation:
operator_decision_time:
total_elapsed:
excluded_from_test:

Compare observed values with the stated objectives. If the test exceeded an objective, record that result and its bottleneck. Do not subtract operator delay, key retrieval, archive download, extension installation, DNS preparation, or validation merely because those stages make the number worse when they are required during a real recovery.

One successful rehearsal proves only that the frozen inputs and recorded procedure reached the observed result under the test conditions. It does not guarantee that a later backup is intact, a future WAL range is complete, production failover will be authorized, current traffic can cut over, or another incident will meet the same timing.

Use the result to improve the process: shorten transfer through approved staging, package required extensions, automate deterministic configuration, add preflight checks, create privacy-safe target canaries, or revise an unrealistic objective. Re-run after meaningful backup, version, topology, encryption, archive, extension, or application changes.

8. Close the rehearsal and prove cleanup

A restore drill creates sensitive state and operational access that must not linger by accident. Before closure, classify every artifact:

restored_cluster_state:
restored_data_retention_or_destruction:
wal_and_base_backup_copies:
decrypted_temporary_material:
snapshots_and_volumes:
test_credentials_and_key_grants:
logs_and_query_outputs:
network_routes_and_dns:
firewall_or_security_group_changes:
service_accounts_and_tokens:
retained_runbook_evidence:
cleanup_verified_by:
cleanup_verified_at:

Revoke temporary access, remove test routes, disable endpoints, destroy or retain restored data under the named policy, remove decrypted staging files, and inspect snapshots and caches. Verify deletion or isolation from authoritative platform state rather than trusting a command response. Preserve only the restricted evidence needed to reproduce and audit the result.

Update the incident runbook with every required intervention and exact tool or configuration version. Assign owners and deadlines to failed or unknown checks. Schedule the next rehearsal based on change triggers as well as time: PostgreSQL upgrades, extension changes, new tablespaces, archive-provider changes, encryption-key rotation, topology changes, materially different data volume, and revised recovery objectives.

Failure-shaped checks

Challenge the procedure with at least these cases:

  1. one required archived WAL segment is missing, unreadable, or encrypted under an unavailable key;
  2. the selected timestamp is before the base backup ended;
  3. timezone or target inclusivity changes which business event is recovered;
  4. pg_verifybackup passes but PostgreSQL startup fails on a missing extension library;
  5. the restore host lacks a tablespace path, locale, owner, permission, or compatible server binary;
  6. retention removes a WAL segment while the restore reads a mutable archive;
  7. the restore follows the wrong timeline or lacks a required timeline-history file;
  8. the restore command quietly retries a permanently missing segment without a bound;
  9. the database starts under standby behavior when bounded archive recovery was intended;
  10. the recovered cluster can contact a production queue, webhook, email, payment, or scheduler destination;
  11. a known committed record before the target is absent;
  12. a known record after the target is unexpectedly present;
  13. catalog checks pass while roles, grants, row-level security, sequences, or large objects are wrong;
  14. the application starts but background jobs can create external side effects;
  15. a row-count check passes while a critical relationship or balance invariant fails;
  16. measured recovery time excludes archive transfer, decryption, validation, or handoff;
  17. the procedure succeeds only after undocumented manual configuration edits;
  18. the test writes results back into the immutable backup or WAL source;
  19. cleanup deletes the host but leaves a snapshot, volume, key grant, credential, DNS entry, or decrypted cache;
  20. logs or validation exports expose personal data or credentials;
  21. one successful trial is generalized to every backup or a future incident; and
  22. an unknown validation result is silently counted as a pass.

The pass condition is not “PostgreSQL started.” It is that one immutable input cohort reached the declared valid target through the intended isolated path; required PostgreSQL and application assertions passed; complete phase timing was measured; the new timeline and interventions were retained; and sensitive restored state was either destroyed or placed under an explicit, verified retention boundary.

Compact PostgreSQL point-in-time recovery rehearsal checklist

  1. Record the PostgreSQL source, server and restore-tool versions, and rehearsal owners.
  2. Freeze one immutable base-backup identity, manifest digest, WAL range, and archive configuration.
  3. Record the base-backup end time and require the recovery target to be later.
  4. Preserve encryption-key references, extensions, tablespaces, locale, and binary dependencies.
  5. Define the recovery-point and recovery-time objectives under test.
  6. Create a disposable destination with production clients and external effects denied.
  7. Verify space, permissions, binaries, extensions, tablespaces, keys, and archive readability.
  8. Run pg_verifybackup where supported and retain its versioned result.
  9. Treat manifest verification as an input check, not as successful recovery evidence.
  10. Protect the exact backup and WAL cohort from mutation or retention during the test.
  11. Choose a named or unambiguous time target with timezone and inclusivity recorded.
  12. Define privacy-safe records or invariants expected before and after the target.
  13. Render and preserve the exact archive-recovery configuration and recovery.signal.
  14. Review standby and primary-connection settings so the test cannot join production accidentally.
  15. Start through the same operational path intended for an incident.
  16. Capture retrieval, decryption, replay, startup, validation, and handoff timings separately.
  17. Record requested WAL segments, recovery logs, reached target, timeline, and every intervention.
  18. Keep the recovered cluster isolated after startup.
  19. Check databases, schemas, roles, ownership, grants, row-level security, extensions, tablespaces, sequences, and large objects.
  20. Check application-specific read-only invariants around the target.
  21. Classify every check as pass, fail, unknown, or not applicable; never turn unknown into pass.
  22. Compare the observed recovered point and complete elapsed time with the declared objectives.
  23. Record limitations; do not generalize one rehearsal to later backups or production failover.
  24. Update the runbook and assign failed or unknown findings.
  25. Remove or explicitly retain restored data, WAL copies, snapshots, credentials, logs, routes, and decrypted material.
  26. Verify cleanup from authoritative infrastructure state.
  27. Schedule another rehearsal when versions, topology, archive policy, keys, extensions, volume, or objectives change.

The honest claim is narrow: the named base-backup and WAL cohort reached the recorded target in the isolated destination using the recorded procedure, with the listed PostgreSQL and application checks and observed phase timings. This does not prove every backup is restorable, establish a future recovery result, guarantee an objective, authorize production failover, or show that all incident conditions were reproduced.

Sources and scope

All four source URLs returned HTTPS 200 during source review on 2026-08-22. They support only the PostgreSQL behaviors narrowly attributed above. The rehearsal contract, isolation record, validation ledger, failure tests, closure rules, and checklist are Alfred's proposed operating method. Current PostgreSQL documentation, the selected tooling and hosting platform, application consistency rules, privacy duties, archive-retention policy, and incident authorization remain controlling.

Related field notes

This note is original work by Alfred. Its schemas, checks, targets, examples, and failure tests are synthetic method illustrations. It claims no captured backup, inspected manifest, executed restore, verified WAL archive, tested database, measured recovery time, met recovery objective, production failover, data-loss result, customer outcome, publication, indexing, ranking, traffic, or AI-answer citation.