Backups you have actually restored
A backup job that prints "ok" every night tells you very little. This lesson turns the order database's backups into something you can prove: you will measure your real recovery numbers, know what each kind of backup leaves out, pick the right base backup and WAL for a restore, watch the archive, and run a timed restore drill every month.
By the end you can
- Measure the order database's real RPO and RTO and explain where the restore time goes
- Take pg_dump backups that restore completely, including roles and settings
- Explain the WAL archive contract and what happens on the primary when archiving fails
- Choose a base backup, a WAL range and a recovery target for point-in-time recovery
- Monitor backups and run a timed, written restore drill every month
The everyday picture
Every car has a spare tyre in the boot. Most drivers have never touched theirs. Then one rainy night on a dark road they find out three things at once: the spare is flat, the jack is missing, and the special key for the locking wheel nuts is in the other car.
A careful driver does something different. Once a month she checks the spare's pressure. Once, on a sunny Saturday at home, she changed a wheel from start to finish and timed it: 25 minutes, mostly spent finding the wheel-lock key. She now keeps that key with the jack.
Database backups fail in the same three ways. The backup file is broken or incomplete (the flat spare). A tool or a step you need on the day is missing (the jack). And something the backup does not contain, such as the database roles or the encryption key, is somewhere else (the wheel-lock key). The only way to find all three is to do a full restore on a calm day, with a clock running.
Recovery numbers you can measure
You met RPO and RTO in Backups and disaster recovery. As the DBA, your job is to turn those targets into numbers you have measured. For Mangrove's order database the business has agreed on targets for a bad change (someone deletes or corrupts data): lose at most 5 minutes of orders, and be back within 2 hours.
Your measured RPO is the age of the newest file in the WAL archive. The order database is about 800 GB and changes all day, so a 16 MB WAL segment fills in seconds at the evening peak. At 4 a.m. it fills slowly, so archive_timeout = '60s' forces a switch at least once a minute. If archiving breaks, the RPO grows every minute until someone notices. That is why archive age is an alert, as you will see below.
Your measured RTO is the sum of every step, end to end. A restore is a chain of slow steps that cannot overlap much:
- Notice and decide. Someone has to see the problem, find the time of the mistake and choose to restore. Often 15 to 30 minutes.
- Fetch the base backup. 800 GB from object storage at roughly 250 MB/s is about 55 minutes. Parallel transfer and compression help a lot. Network speed sets the limit.
- Replay the WAL. Every WAL file since the base backup is replayed, one change after another, by a single recovery process. The order database grows only about 1.5 GB a day, but it writes far more WAL than that, roughly 30 GB on a normal day, because every update, every index change and the first change to each page after a checkpoint are all logged. Six days since the last full backup means about 180 GB of WAL to replay.
- Check the data and switch the app. Row counts, the newest order time, a smoke test, then pointing the API at the new server.
Before the drill, everyone guessed "about an hour". The guess left out WAL replay, which depends on how old the newest base backup is. So the backup schedule sets your RTO as much as the restore tools do.
Quick check
Archiving works and archive_timeout is 60 seconds. Full base backups run every Sunday. On Saturday you are asked to make restores faster. Which change helps the RTO most?
By Saturday a restore must replay six days of WAL, the slowest step. A nightly backup limits replay to less than a day. A shorter archive_timeout improves the RPO, the amount of data you can lose, and does nothing for restore time. Longer retention lets you go further back but does not make any restore faster.
pg_dump done properly: what a dump holds and what it leaves out
A logical dump is your second, independent kind of backup: it can restore one table, move data to another major version, and does not depend on the WAL chain. To make one that restores completely on an empty server, you need to know four things.
Pick the right format. The custom format (-Fc) is one compressed file. The directory format (-Fd) writes one file per table into a folder. Only the directory format lets pg_dump itself run in parallel with -j. Both formats let pg_restore -j restore in parallel, and both let you list the contents with pg_restore -l and restore one table with -t. A plain .sql file can do none of that.
Know what lives outside the database. pg_dump saves one database. Roles, their passwords and tablespaces belong to the whole server, so they are not in it. Dump them separately with pg_dumpall --globals-only and restore that file first, or every GRANT in the dump fails with "role does not exist". Settings made with ALTER DATABASE … SET and grants on the database itself are only included when you dump with --create. The configuration files (postgresql.conf, pg_hba.conf) are in no dump at all. Neither are extension binaries: the restore server needs the same extensions installed.
A dump holds a snapshot open. pg_dump reads everything inside one transaction, so the dump is consistent. For the 2 or 3 hours a full dump of the order database takes, that open snapshot stops VACUUM from removing dead rows anywhere in the database (Running Postgres in production explains why). Run big dumps from a replica when you can, or at quiet hours.
A dump takes locks. It holds a shared lock on every table it dumps until it finishes. Reads and writes carry on, but any ALTER TABLE must wait, and every query that arrives after the waiting ALTER queues behind it (Changing the schema without downtime). Do not schedule dumps and migrations at the same time. --lock-wait-timeout makes the dump give up instead of waiting forever for its locks at the start.
Quick check
You restore last night's orders.dump into a fresh server. Tables and rows arrive, but the log is full of "role orders_app does not exist" and the app cannot log in. What was missing?
Roles belong to the whole server, so no single-database dump contains them, whatever its format or restore options. Restore the globals file first, then the dump, and the grants work. Parallel jobs only change how fast the restore runs.
Base backups and the archive contract
For point-in-time recovery you need a physical base backup plus every WAL file after it. pg_basebackup copies the data directory while the server runs. By default (-X stream) it also streams the WAL written during the copy, so the backup can be made consistent. Since PG 13 it writes a backup manifest, and pg_verifybackup checks the files against it (which backup formats it can check is version dependent). PG 17 added incremental base backups: pg_basebackup --incremental copies only blocks changed since an earlier backup, needs summarize_wal = on, and pg_combinebackup rebuilds a full copy before a restore.
The WAL side is a contract between PostgreSQL and your archive_command (or an archive_library module, PG 15 and later). When a segment is full, PostgreSQL writes a small .ready marker for it. The archiver process runs your command for that file. Your command must return exit code 0 only when the file is safely and durably stored, and it must never overwrite a different file with the same name. On exit 0, PostgreSQL marks the segment .done, and a later checkpoint may recycle it. On any other exit code, PostgreSQL waits and tries the same file again, forever. It never skips a file, because one gap would break every restore after it.
pg_wal directory on the primary.This is why a backup problem can take production down. If the archive bucket's credentials expire on Friday evening, nothing breaks at first. WAL piles up in pg_wal, roughly 30 GB a day on the order database, until the disk is full and PostgreSQL stops accepting writes. Two good habits follow. Never "fix" a stuck archive by deleting files from pg_wal; repair the command and let it catch up. And never use a command that returns 0 without really storing the file, such as archive_command = '/bin/true', on a server whose backups you rely on: every restore will hit a gap.
Quick check
pg_wal on the order database primary has grown from 4 GB to 60 GB since last night. pg_stat_archiver shows failed_count rising and last_archived_time from 18 hours ago. What is the right first move?
The files are kept because they have not been archived. Deleting them breaks the WAL chain, so no restore can pass that point, and it can corrupt the server if it still needs them. A checkpoint cannot recycle files that are not marked done. Fixing the command lets PostgreSQL archive the backlog and free the space by itself.
Point-in-time recovery: picking the backup, the WAL and the target
A PITR restore needs three choices, made before you touch any server.
The base backup. Take the newest successful base backup that finished before your target time. A backup that started before the target but finished after it cannot be used: PostgreSQL cannot stop recovery at a moment when that backup was still being copied, because the copy is not consistent until its end point.
The WAL range. You need every segment from the one where the backup started to the one that contains the target time, with no gaps. Check this before you start. Discovering a missing file two hours into WAL replay costs you the whole restore. If a file is missing, look for it in the second repository or in pg_wal on the old primary before you begin.
The target. recovery_target_time is the usual choice. You can also stop at a transaction ID (recovery_target_xid), a WAL position (recovery_target_lsn) or a named restore point (recovery_target_name). Only one target may be set. Restore points are a cheap habit before risky changes: run SELECT pg_create_restore_point('before_0042') before migration 0042, and if it goes wrong you can stop recovery exactly there. recovery_target_inclusive (on by default) decides whether a transaction that commits exactly at the target is kept. recovery_target_action decides what happens at the target: pause (the default) lets you check the data read-only first, promote ends recovery and opens for writes, shutdown stops the server.
When recovery ends, the server starts a new timeline and writes a small history file to the archive. If your first restore stopped too late and you restore again from the same archive, the timelines keep the two histories apart. Since PG 12 the default recovery_target_timeline = 'latest' follows the newest branch. Leave the default unless you know you want an older branch.
Tools, monitoring and the monthly drill
Hand-written cp archiving is fine for learning. In production a backup tool does the hard parts. pgBackRest, WAL-G and Barman all take base backups (full plus differential or incremental), archive WAL in parallel, compress it, encrypt it, and copy it to object storage or a backup server. Each also enforces retention: when an old full backup expires, the tool also deletes the WAL that only that backup needed, and keeps everything a newer backup still depends on. Each can also verify what it stored: pgBackRest has check and verify commands, and Barman has check. The choice matters less than running one of them, configured, monitored and restored from.
Backups fail quietly, so alert on what you can measure. Four signals cover most failures:
- Archive age: now minus
last_archived_timefrom pg_stat_archiver. Alert when it passes your RPO. - Archiving failing now:
last_failed_timenewer thanlast_archived_time. A non-zerofailed_countalone may be an old, recovered failure. - Backlog: the number of
.readyfiles waiting, frompg_ls_archive_statusdir()(PG 12 and later), and the size ofpg_wal. - Base backup age: the newest successful base backup should be under about 26 hours old with nightly backups. Alert on silence, not only on errors: a cron job that never starts prints no error.
Then comes the part most teams skip: the restore drill. Once a month, restore the order database to a chosen minute from yesterday onto a scratch server, with a clock running. Check row counts and the newest order time against production. Write down the backup used, the time of each step, the total, and what surprised you. Compare the total with the 2-hour target, fix the biggest gap, and repeat next month. The written record is your evidence: it is what you show an auditor, and what the next on-call engineer reads at 3 a.m.
On a managed service (RDS, Cloud SQL, Azure Database for PostgreSQL) the provider takes base backups, archives WAL and offers PITR to any moment in a retention window that you set, up to a provider maximum. A PITR restore there creates a new instance with a new address. Several jobs stay yours: choosing the retention, turning on a copy in another region or account if you need one, keeping logical dumps for long-term or exit needs, and timing restores, because the provider does not promise how long a restore of 800 GB takes.
Build it
We will make a complete logical backup, set up pgBackRest with a second repository, add backup monitoring, use a restore point, and script the monthly drill. Commands assume PostgreSQL 16 on Ubuntu with pgBackRest 2.50 or later.
-
A logical backup that restores on an empty server
Globals first, a parallel directory-format dump, a copy of the config, then a test restore in the same order.
Terminal# Roles, passwords and tablespaces live outside any one database pg_dumpall -h 10.0.2.10 -U postgres --globals-only -f /backup/globals_2026-10-15.sql # Directory format, 4 parallel jobs; give up if a migration holds a lock for 30 s pg_dump -h 10.0.2.10 -U postgres -Fd -j 4 --lock-wait-timeout=30s \ -f /backup/orders_2026-10-15 orders # The config files are in no dump: keep a copy beside it (run as root) tar -czf /backup/orders_2026-10-15.etc.tgz /etc/postgresql/16/main # Test restore on a scratch server: globals, then an empty database, then the data psql -h 10.0.9.50 -U postgres -f /backup/globals_2026-10-15.sql # "role postgres already exists" is expected createdb -h 10.0.9.50 -U postgres orders pg_restore -h 10.0.9.50 -U postgres -d orders -j 8 /backup/orders_2026-10-15 -
pgBackRest with two repositories and retention
Repository 1 is in the same region for fast restores. Repository 2 is in another region under another account. Archived WAL goes to both. Keep the S3 keys and cipher passwords in environment variables such as
PGBACKREST_REPO1_CIPHER_PASS, never in this file.INI# /etc/pgbackrest/pgbackrest.conf on the order database primary [global] repo1-type=s3 repo1-s3-bucket=mangrove-pg-main repo1-s3-endpoint=s3.ap-southeast-1.amazonaws.com repo1-s3-region=ap-southeast-1 repo1-path=/orders repo1-retention-full=2 # keep 2 full backups and the WAL they need repo1-retention-diff=6 # and the last 6 differentials repo1-cipher-type=aes-256-cbc repo2-type=s3 repo2-s3-bucket=mangrove-pg-dr repo2-s3-endpoint=s3.eu-west-1.amazonaws.com repo2-s3-region=eu-west-1 repo2-path=/orders repo2-retention-full=4 repo2-cipher-type=aes-256-cbc process-max=8 # parallel compression and transfer compress-type=zst archive-async=y [orders] pg1-path=/var/lib/postgresql/16/mainOutput# postgresql.conf: archive_command = 'pgbackrest --stanza=orders archive-push %p' # crontab for postgres: full on Sunday, differential other nights, repo 2 weekly 0 1 * * 0 pgbackrest --stanza=orders --type=full backup 0 1 * * 1-6 pgbackrest --stanza=orders --type=diff backup 0 5 * * 0 pgbackrest --stanza=orders --repo=2 --type=full backupWith several repositories,
backupwrites to repository 1 unless you pass--repo. Runpgbackrest --stanza=orders checkonce after setup: it forces a WAL switch and confirms the file reaches the archive. -
Monitor the archive and the backups
Run this query from your monitoring agent every minute, with a role that is a member of
pg_monitor.SQLSELECT now() - last_archived_time AS archive_age, failed_count, last_failed_time > last_archived_time AS failing_now, -- NULL if it never failed (SELECT count(*) FROM pg_ls_archive_statusdir() WHERE name LIKE '%.ready') AS files_waiting FROM pg_stat_archiver;Outputarchive_age | failed_count | failing_now | files_waiting -----------------+--------------+-------------+--------------- 00:00:41.203118 | 3 | f | 0Three old failures, but the last success is newer than the last failure and nothing is waiting: healthy. For base backup age, read pgBackRest's JSON output and alert above 26 hours (93,600 seconds):
Terminal# Seconds since the newest backup in any repository finished pgbackrest --stanza=orders --output=json info \ | jq 'now - ([.[0].backup[].timestamp.stop] | max)' -
Leave a restore point before risky changes
Add this to the start of every risky migration's runbook. It costs one small WAL record.
SQLSELECT pg_create_restore_point('before_0042_order_status');Terminal# If migration 0042 goes wrong: restore a copy to exactly that point sudo -u postgres pgbackrest --stanza=orders --delta \ --type=name --target=before_0042_order_status --target-action=pause restore -
Script the monthly restore drill
It runs on the scratch server
drill-01, restores to yesterday at 19:30, waits until recovery has really finished, checks the data and records the time.Terminal#!/usr/bin/env bash # /usr/local/bin/restore-drill.sh: run monthly as root on drill-01 set -euo pipefail TARGET="$(date -u -d 'yesterday 19:30' '+%F %T')+00" # a busy evening minute START=$(date +%s) systemctl stop postgresql # This copy must never push WAL into the production repositories echo "archive_mode = off" > /etc/postgresql/16/main/conf.d/drill.conf sudo -u postgres pgbackrest --stanza=orders --delta \ --type=time --target="$TARGET" --target-action=promote restore systemctl start postgresql # Accepting connections is not enough: wait until recovery has ended until [ "$(sudo -u postgres psql -Atc 'SELECT pg_is_in_recovery()' 2>/dev/null)" = "f" ]; do sleep 30 done END=$(date +%s) # The newest order must be just before the target time sudo -u postgres psql -d orders -Atc "SELECT count(*), max(created_at) FROM orders" echo "$(date -u +%F),$TARGET,$(( (END - START) / 60 ))" >> /var/log/restore-drill.csvOutput61402213|2026-10-13 19:29:58.41+00 # /var/log/restore-drill.csv: date, target, restore minutes 2026-09-15,2026-09-14 19:30:00+00,195 2026-10-14,2026-10-13 19:30:00+00,55The script times the restore itself. Add the time to notice, decide, check and switch the app (about 50 to 55 minutes for the order database) to get the full RTO you report. After the drill, wipe the scratch server: it holds every customer's orders.
Bring it to your team
Ask these in your next database review, and again after any incident.
- "When did we last restore the order database onto an empty server, and how long did each step take?"
- "If archiving failed at 6 p.m. on Friday, which alert fires, and how long before the disk fills?"
- "Where are the globals dump, the config files and the cipher password, and can the on-call engineer reach them without the person who set them up?"
- "Which base backup would we use to restore to 19:30 yesterday, and have we checked the WAL chain to that point is complete?"
- "On the managed service: what retention did we set, is there a copy in another region, and have we timed a PITR restore there?"
Red flags
- The only proof of backups is a nightly "ok" email; nobody has a written drill result.
- Someone suggests deleting files in
pg_walto free space. archive_commandreturns 0 without checking that the copy succeeded.- A drill script that stops the clock when the server accepts connections.
- Big dumps scheduled at the same time as migrations, on the primary.
Practice
Plan a restore from the backup catalog
At 19:40:31 on 14 October a bad UPDATE corrupted order statuses. You will restore a copy of the order database to 2026-10-14 19:40:00+00. base_backup lists the base backups. wal_archive lists archived WAL segments by number (real file names encode this number in hex); a segment's archived_at is when it filled up and was archived, so the segment that contains the target time is the first one archived at or after it. To keep the data small, it shows about one segment an hour. Return one row: label of the base backup to use (the newest with status ok that stopped before the target), first_seg (that backup's start_seg), last_seg (the segment containing the target), and missing_segs (how many segment numbers in that range are not in the archive).
CREATE TABLE base_backup (label text PRIMARY KEY, started_at timestamptz, stopped_at timestamptz, start_seg int, status text);
INSERT INTO base_backup VALUES
('bb-1012-0200', '2026-10-12 02:00:00+00', '2026-10-12 03:05:00+00', 20, 'ok'),
('bb-1013-0200', '2026-10-13 02:00:00+00', '2026-10-13 03:04:00+00', 30, 'ok'),
('bb-1014-0200', '2026-10-14 02:00:00+00', '2026-10-14 03:06:00+00', 40, 'ok'),
('bb-1014-1200', '2026-10-14 12:00:00+00', '2026-10-14 12:41:00+00', 46, 'failed'),
('bb-1014-1920', '2026-10-14 19:20:00+00', '2026-10-14 19:58:00+00', 51, 'ok');
CREATE TABLE wal_archive (seg int PRIMARY KEY, archived_at timestamptz);
INSERT INTO wal_archive VALUES
(38, '2026-10-13 22:10:00+00'), (39, '2026-10-14 01:30:00+00'), (40, '2026-10-14 02:40:00+00'),
(41, '2026-10-14 05:15:00+00'), (42, '2026-10-14 08:20:00+00'), (43, '2026-10-14 10:05:00+00'),
(45, '2026-10-14 13:10:00+00'), (46, '2026-10-14 14:50:00+00'), (47, '2026-10-14 16:30:00+00'),
(48, '2026-10-14 17:45:00+00'), (49, '2026-10-14 18:40:00+00'), (50, '2026-10-14 19:15:00+00'),
(51, '2026-10-14 19:36:00+00'), (52, '2026-10-14 19:44:00+00'), (53, '2026-10-14 19:52:00+00'),
(54, '2026-10-14 20:01:00+00');
-- Restore target: 2026-10-14 19:40:00+00
SELECT label, start_seg AS first_seg
FROM base_backup
WHERE status = 'ok'
ORDER BY started_at DESC
LIMIT 1;
[{"label": "bb-1014-0200", "first_seg": 40, "last_seg": 52, "missing_segs": 1}]
Show a solution
WITH target AS (SELECT timestamptz '2026-10-14 19:40:00+00' AS t),
pick AS ( -- newest good backup that FINISHED before the target
SELECT b.label, b.start_seg
FROM base_backup b, target
WHERE b.status = 'ok' AND b.stopped_at < target.t
ORDER BY b.stopped_at DESC
LIMIT 1
),
last AS ( -- the segment that contains the target time
SELECT min(seg) AS last_seg
FROM wal_archive, target
WHERE archived_at >= target.t
)
SELECT p.label, p.start_seg AS first_seg, l.last_seg,
(l.last_seg - p.start_seg + 1)
- (SELECT count(*) FROM wal_archive w
WHERE w.seg BETWEEN p.start_seg AND l.last_seg) AS missing_segs
FROM pick p, last l;The 19:20 backup started before the target but finished at 19:58, so it cannot be used. The 12:00 backup failed. So you start from bb-1014-0200 and need segments 40 to 52. Segment 44 is missing: replay would stop around 11:30. Find segment 44 (in the second repository, or in pg_wal on the primary) before you start the restore.
Flag backup problems across the fleet
The table archiver has one row per database cluster, with the same columns as pg_stat_archiver. backup_run lists base backup runs, and target holds each cluster's RPO. Treat the current time as 2026-10-15 09:00:00+00. Return one row per problem, with columns cluster and problem, where problem is one of: archive behind RPO (now minus last_archived_time is more than the RPO), archiving failing (last_failed_time is newer than last_archived_time), base backup too old (no ok backup stopped in the last 26 hours).
CREATE TABLE archiver (cluster text PRIMARY KEY, archived_count bigint, last_archived_wal text, last_archived_time timestamptz, failed_count bigint, last_failed_wal text, last_failed_time timestamptz);
INSERT INTO archiver VALUES
('orders-primary', 91200, '00000001000000A2000000F1', '2026-10-15 08:58:00+00', 3, '00000001000000A0000000B3', '2026-10-12 03:00:00+00'),
('store-primary', 40310, '000000010000004E00000012', '2026-10-15 08:20:00+00', 57, '000000010000004E00000013', '2026-10-15 08:59:00+00'),
('partner-primary', 8120, '000000010000000C0000003A', '2026-10-15 08:55:00+00', 0, NULL, NULL),
('warehouse-staging', 2210, '000000010000000300000044', '2026-10-15 08:10:00+00', 0, NULL, NULL),
('rider-primary', 6400, '000000010000000900000020', '2026-10-15 08:57:00+00', 1, '00000001000000090000001E', '2026-10-15 08:30:00+00');
CREATE TABLE backup_run (cluster text, stopped_at timestamptz, status text);
INSERT INTO backup_run VALUES
('orders-primary', '2026-10-14 03:06:00+00', 'ok'), ('orders-primary', '2026-10-15 03:05:00+00', 'ok'),
('store-primary', '2026-10-15 02:50:00+00', 'ok'),
('partner-primary', '2026-10-13 02:40:00+00', 'ok'), ('partner-primary', '2026-10-14 02:40:00+00', 'failed'),
('partner-primary', '2026-10-15 02:40:00+00', 'failed'),
('warehouse-staging', '2026-10-14 08:00:00+00', 'ok');
CREATE TABLE target (cluster text PRIMARY KEY, rpo_minutes int);
INSERT INTO target VALUES ('orders-primary', 5), ('store-primary', 15), ('partner-primary', 15),
('warehouse-staging', 60), ('rider-primary', 5);
-- Now is 2026-10-15 09:00:00+00
SELECT cluster, 'archiving failing' AS problem
FROM archiver
WHERE failed_count > 0;
[{"cluster": "store-primary", "problem": "archive behind RPO"},
{"cluster": "store-primary", "problem": "archiving failing"},
{"cluster": "partner-primary", "problem": "base backup too old"},
{"cluster": "rider-primary", "problem": "base backup too old"}]
Show a solution
WITH clock AS (SELECT timestamptz '2026-10-15 09:00:00+00' AS now_t)
SELECT a.cluster, 'archive behind RPO' AS problem
FROM archiver a JOIN target g ON g.cluster = a.cluster, clock
WHERE clock.now_t - a.last_archived_time > g.rpo_minutes * interval '1 minute'
UNION ALL
SELECT cluster, 'archiving failing'
FROM archiver
WHERE last_failed_time > last_archived_time -- NULL (never failed) is not true
UNION ALL
SELECT g.cluster, 'base backup too old'
FROM target g, clock
WHERE NOT EXISTS (SELECT 1 FROM backup_run b
WHERE b.cluster = g.cluster AND b.status = 'ok'
AND b.stopped_at > clock.now_t - interval '26 hours');orders-primary and rider-primary have failures in their history, but their last success is newer, so they archive fine today. rider-primary has never had a base backup at all, which only the NOT EXISTS form catches. warehouse-staging is 50 minutes behind against a 60-minute RPO, and its backup is 25 hours old: both inside the limits.
Check yourself
The order database's managed service offers PITR with 7 days of retention, and backups are "automatic". A finance manager asks whether you can still restore if the whole cloud region is lost for a day. What is the most accurate answer?
By default managed backups usually live in the same region as the instance, so losing the region can mean losing them too. A cross-region or cross-account copy is an option you must turn on and test. Longer retention lets you go back further in time; it does not move the copy anywhere safer.
A drill restores the order database onto a new server from pg_dump files only. Tables and rows are complete, but logins fail and work_mem is back at its default. What would have prevented both problems?
Roles live at the server level and settings live in the configuration files, so a single-database dump contains neither. Directory format and parallel jobs only change speed. --no-owner hides ownership errors but does not create the roles the app logs in with. A dump from a replica has the same contents.
You run SELECT pg_create_restore_point('before_0042') and then migration 0042, which drops the wrong column. Production keeps taking orders. What is the best recovery plan?
The restore point lets a copy stop exactly before the migration, and copying one column back keeps every order taken since. Restoring production itself throws those orders away. The replica replayed the drop within moments. Last night's dump is many hours older than the restore point and would overwrite newer changes.
Flashcards
Key takeaways
- Measure your recovery numbers: RPO is the archive's age, RTO is notice + fetch + replay + checks + switch. Base backup frequency drives replay time.
- A complete logical backup is the dump plus the globals file plus the config files. Dumps hold a snapshot and table locks, so keep them away from migrations.
- The archive contract: exit 0 only when the file is safe. Failing archiving keeps WAL on the primary until the disk fills, so never delete files from
pg_wal. - For PITR, pick the newest good backup that finished before the target, check the WAL chain has no gaps, and restore onto a spare server with archiving off.
- Alert on archive age, failing archiving, backlog and base backup age, and run a timed, written restore drill every month.
Sources
- Continuous archiving and point-in-time recovery, PostgreSQL documentation
- pg_dump, PostgreSQL documentation
- Write ahead log settings, including archiving and recovery targets, PostgreSQL documentation
- pgBackRest user guide, pgBackRest project