Skip to main content
RunBook Academy

← All checklists in Backup & DR

As neededbdr-post-restore

Post-restore validation

22 items Β·15 critical Β·7 warn Β·0 info

How to use

Run this once, on the recovered system, after the restore has finished and before anybody announces that the service is back. It is not a readiness review and it does not repeat quarterly; it belongs to one incident and it closes with that incident.

Work it top to bottom without reordering. The sequence is deliberate: bytes first, then the meaning of those bytes, then the point in time they represent, then the surrounding services, then the path a user takes, then the instrumentation that will tell you about the next failure. Every step depends on the ones above it, and an application transaction that succeeds says nothing useful while the file-level verification is still unrun.

Two questions run underneath the whole list. Is the recovered system correct, or is it merely running? And can the answer be shown to somebody who was not in the room? An item passes on an artefact β€” a status, a path, a number, a timestamp β€” and never on an operator’s recollection of a terminal that has since been closed.

Where the numbers come from

The verification items come from a measured run. A restore whose per-file comparison succeeds looks like this, and the second exit status is the one that carries the evidence.

Read-only / Safea restore and the verification that makes it a result
$ restic restore 3fe43af4 --target /work/restore && md5sum -c manifest.md5
restoring snapshot 3fe43af4 of [/work/prod] at 2026-08-28 13:27:02.65376235 +0000 UTC by root@8211a08b55c3 to /work/restore
Summary: Restored 7 files/dirs (60.000 MiB) in 0:00
>>> exit code: 0

--- comparing the restored tree against the 09:00 checksums ---
./app/app.conf: OK
./app/orders.csv: OK
./db/data.bin: OK
>>> md5sum -c exit code: 0

Note what the first line supplies for free: the recovery point actually being restored, printed by the restore itself. That is the value the recovery-point items ask for, and reading it costs nothing.

The point-in-time items come from a separate capture on PostgreSQL 18.6, where a base backup was rolled forward past an unqualified DELETE to a chosen target time.

Service impact possiblethe point requested and the point reached are different numbers
$ pg_ctl -D /work/pitr start   # recovery_target_time set to 2026-08-28 13:34:40.077562+00
  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
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+00

Three distinct instants appear there: the one requested, the one recovery stopped before, and the one actually held. They differ by seconds here and can differ by much more. The recovery was accepted because the recovered database returned rows recovered : 50000 (expected 50000) and sum(amount) : 825025000 (expected 825025000) β€” a property of the business data recorded before the incident, not the observation that the server had started.

Elapsed times come from timestamps written as each stage ended. Nothing on this list is estimated afterwards.

Access this needs

Read access to the recovered filesystem and to the backup-time manifest, from the recovery host. Read access to the recovery log of whatever performed the restore, and to the incident record where the statuses were captured.

Database or application credentials sufficient to evaluate the business invariant and to execute one write transaction end to end, plus agreement from the service owner that a synthetic record may be created and then removed.

Sight of the monitoring configuration and the standing to trigger one deliberate failure. Write access to the runbook repository, because the last item is a commit.

No item requires write access to the backup repository.

What the review produces

A dated section appended to the incident record, naming the operator, listing every item and its disposition. Six values belong at the top: the restore exit status, the verification exit status, the recovery point actually reached, the business invariant with its expected and observed values, the work lost behind the recovery point, and the total elapsed time with its per-stage breakdown.

Two of those leave the engineering team. The recovery point and the work lost behind it go to the named service owner in writing before the incident closes. The measured elapsed time goes to whoever owns the recovery time objective, whether or not it is comfortable reading.

One artefact stays: the runbook commit. An incident that ends without one has converted a day of expensive learning into a story.

Sign-off

  • Reviewer: ____
  • Date: ____
  • Service owner (recovery point and lost work accepted): ____
  • Runbook commit: ____

Every critical item passes, or the recovered system stays out of service until a named owner accepts the residual risk in writing with a remediation date.

Critical15 items

  1. grep -n "rbdr-restore-exit=" /srv/rbdr-incidents/rbdr-inc-2026-08-28.log
  2. ( cd /srv/rbdr-restore && md5sum -c /srv/rbdr-manifests/rbdr-2026-08-28.md5 ); echo "rbdr-verify-exit=$?"
  3. psql -h /var/run/postgresql -p 5434 -Atc "SELECT count(*), sum(amount) FROM rbdr_orders"
  4. grep -E "last completed transaction|recovery stopping before" /srv/rbdr-restore/log/postgresql.log
  5. curl -sS -X POST --data @/srv/rbdr-probes/rbdr-canary-order.json http://rbdr-recovered.internal/orders
  6. systemctl --no-pager is-active rbdr-node-exporter
  7. restic -r "$RBDR_REPO" check --read-data
  8. grep -n "rbdr-stage" /srv/rbdr-incidents/rbdr-inc-2026-08-28.log
  9. git -C /srv/rbdr-runbooks log --oneline -1 -- restore-orders.md

Warning7 items

  1. wc -l < /srv/rbdr-manifests/rbdr-2026-08-28.md5; find /srv/rbdr-restore -type f | wc -l
  2. grep -n "selected new timeline ID" /srv/rbdr-restore/log/postgresql.log