This episode tests recovery in three real scenarios: a corrupt cluster recovered to the last backup plus WAL, the loss of the entire data directory requiring a rebuild from scratch, and creating a clone or new standby instance from a backup. It ends with a crash simulation and PITR to a specific time exercise.

In episode 8 we mastered the restore command. Now comes the real test: recovery scenarios in real situations. Because disasters never arrive as neat cases — a cluster corrupts in the middle of peak hours, the data directory is lost after a disk failure, or there's a sudden need for a second instance. In episode 9 we train three scenarios: corrupt, lost, and clone, then close with a crash simulation and PITR you can repeat on your own.
This is why a good DBA differs from an "adequate" one: not because they can run restore, but because they've trained disaster scenarios before the disaster comes.
PostgreSQL can't start, or shows corruption errors like invalid page in block or could not read block. The server still exists, the data still exists, but it's broken.
Since the data is corrupt, trusting local files makes no sense — we overwrite with the backup using --delta so only the different files are replaced:
sudo systemctl stop postgresql
sudo -u postgres pgbackrest --stanza=main --delta restore
sudo systemctl start postgresqlImportant note: if the corruption comes from physical disk (bad sectors, corrupt filesystem), don't restore to the same disk — that only copies the problem. Move PGDATA to a healthy disk, then restore.
Note
--delta here is the right choice because the surviving uncorrupted files are reused, speeding up recovery. But if you suspect the corruption has spread, it's safer to do a full restore to a new directory (Scenario 2) than to mix in local files that might be damaged.
/var/lib/postgresql/16/main is gone or unreadable — dead disk, accidental format, or a deleted instance. Nothing can be salvaged locally; everything must be rebuilt from the repository.
sudo systemctl stop postgresql
sudo rm -rf /var/lib/postgresql/16/main
sudo -u postgres pgbackrest --stanza=main --type=immediate restore
sudo systemctl start postgresqlNote: restore recreates the entire PGDATA including postgresql.conf (the backed-up version) and the PG_VERSION file. If server parameters were changed after the last backup, you'll need to realign them — postgresql.auto.conf and other config files outside PGDATA are not included in the backup.
After the restore, check important parameters that might differ from expectations:
psql -U postgres -c "SHOW archive_mode; SHOW listen_addresses;"Adjust postgresql.conf/postgresql.auto.conf if needed, then restart once more. This procedure is full recovery — losing the entire data directory no longer means losing data.
You need a second instance: a staging environment close to production, analytics without burdening the primary, or a new standby for HA (covered in depth in episode 18). All of these can be cloned from the same backup — without touching the primary.
Restore to a separate directory with --delta against an empty PGDATA:
sudo mkdir -p /var/lib/postgresql/staging
sudo chown postgres:postgres /var/lib/postgresql/staging
sudo -u postgres pgbackrest --stanza=main --delta --pg1-path=/var/lib/postgresql/staging restore--pg1-path points the restore at a different directory than the config. After the restore, adjust the port and socket in the clone's postgresql.conf to avoid conflicts, then start it as a separate instance:
sudo -u postgres /usr/lib/postgresql/16/bin/pg_ctl -D /var/lib/postgresql/staging startTip
This is a pattern reused over and over in production: staging that's always "fresh" from the last backup, analytics offload, or bypassing the slow pg_dump. Understanding cloning from a backup means understanding that backups aren't only for disasters — they're raw material for new environments.
Let's combine everything into one exercise you can repeat anytime.
CREATE TABLE uji_pit (id int primary key, ts timestamptz default now());
INSERT INTO uji_pit (id) SELECT generate_series(1,100);
SELECT pg_create_restore_point('before-crash');INSERT INTO uji_pit (id) SELECT generate_series(101,200);Then stop the cluster forcefully — simulating a crash without a clean shutdown:
sudo systemctl stop postgresqlRecover to the before-crash restore point (only the 100 rows that existed then):
sudo -u postgres pgbackrest --stanza=main --type=name 'before-crash' --target-action=pause restore
sudo systemctl start postgresqlSELECT count(*) FROM uji_pit; -- should be 100, not 200If the result is 100, PITR works perfectly. Repeat the exercise with --type=time or --type=lsn to practice the other variants.
Warning
A crash simulation without a tested backup is the main reason for recovery failures in the real world. Make this exercise a monthly routine (episode 16 covers scheduled drills) — and always test on a separate instance, never on production directly.
Key takeaways:
--delta restore, restart — move to a healthy disk if the cause is physical.restore --type=immediate, then realign the configuration.--pg1-path for staging, analytics, or a standby.In the next episode we'll make all of this automatic: scheduling & retention policy — scheduling weekly fulls and daily differentials via cron or a systemd timer, understanding the role of real-time WAL archiving, and determining retention values based on your business RPO and RTO. A reliable backup shouldn't depend on human memory!