1. Courses
  2. Engineering Roles
  3. Database Administration with PostgreSQL
  4. Stop 2

40 minIntermediate

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 and 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 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.
RTO target 2 h Notice, decide 20 min Fetch base backup 55 min Replay 6 days WAL 140 min Check the data 20 min Switch the app 15 min 0 2 h 4 h Measured: 4 h 10 min
The first timed restore drill of the order database, step by step. Each step starts when the one before ends. Replaying six days of WAL took longer than everything else together, so the fix that mattered most was more frequent base backups.

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?

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 (-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?

Base backups and the archive contract

For point-in-time recovery you need a physical 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 , 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.

WAL segment full 1. mark .ready archiver archive_command exit 0 non-zero copy safe in repo file marked .done retry, keep file in pg_wal retry at checkpoint disk fills segment recycled primary stops Alert on failed_count and on archive age
The archive contract. Exit code 0 means "this file is safe", so PostgreSQL may recycle it. Any failure makes PostgreSQL keep the file and retry. A broken bucket permission or a full archive disk therefore shows up first as a growing 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?

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 (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 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_time from . Alert when it passes your RPO.
  • Archiving failing now: last_failed_time newer than last_archived_time. A non-zero failed_count alone may be an old, recovered failure.
  • Backlog: the number of .ready files waiting, from pg_ls_archive_statusdir() (PG 12 and later), and the size of pg_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 . 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.

  1. 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
  2. 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/main
    Output
    # 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 backup

    With several repositories, backup writes to repository 1 unless you pass --repo. Run pgbackrest --stanza=orders check once after setup: it forces a WAL switch and confirms the file reaches the archive.

  3. 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.

    SQL
    SELECT 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;
    Output
       archive_age   | failed_count | failing_now | files_waiting
    -----------------+--------------+-------------+---------------
     00:00:41.203118 |            3 | f           |             0

    Three 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)'
  4. Leave a restore point before risky changes

    Add this to the start of every risky migration's runbook. It costs one small WAL record.

    SQL
    SELECT 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
  5. 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.csv
    Output
    61402213|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,55

    The 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_wal to free space.
  • archive_command returns 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).

SQL exerciseRuns in a real PostgreSQL inside your browser
-- 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;
Ctrl + Enter runs
Show a solution
SQL
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.

Pass the exercise to finish this task.

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).

SQL exerciseRuns in a real PostgreSQL inside your browser
-- Now is 2026-10-15 09:00:00+00
SELECT cluster, 'archiving failing' AS problem
FROM archiver
WHERE failed_count > 0;
Ctrl + Enter runs
Show a solution
SQL
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.

Pass the exercise to finish this task.

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?

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?

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?

Flashcards

Open my review

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

Episode transcriptThe drill that said four hours · about 12 minutes · tap any line to play from there
  1. NadiaImran, if the order database lost an hour of data tonight, how long would it take us to get it back?
  2. ImranI'd say about an hour. That's what Ken told finance last quarter.
  3. NadiaThat's what everyone said. Then we timed it. Four hours and ten minutes.
  4. ImranFour hours? On festival night that's the whole evening rush.
  5. NadiaSo that's today's episode. Backups you've actually restored. How to measure your real recovery numbers, what a backup quietly leaves out, how to pick the right pieces for a restore, and the monthly drill that keeps everyone honest.
  6. ImranStart with a picture, please.
  7. NadiaThink of the spare tyre in your car. Most people have never touched theirs. Then one rainy night they find out three things at once. The spare is flat, the jack is missing, and the key for the locking wheel nuts is in the other car.
  8. ImranThat's my brother. Exactly that, last year.
  9. NadiaA careful driver checks the pressure once a month. And once, on a sunny Saturday at home, she changes a wheel from start to finish with a timer running. She learns it takes twenty-five minutes, mostly looking for the wheel-lock key.
  10. ImranSo the flat spare is a broken backup file. The missing jack is a tool or step you need on the day. What's the wheel-lock key?
  11. NadiaSomething the backup doesn't contain. The database roles and their passwords, the config files, the encryption key. You only find all three problems by doing a full restore on a calm day, with a clock running.
  12. ImranOkay. You said measure. What exactly do we measure?
  13. NadiaTwo numbers from the backups episode. RPO, how much recent data we may lose. And RTO, how long we may be down. For the order database, the business agreed on five minutes of orders and two hours of downtime, for a bad change like someone corrupting data.
  14. NadiaWith WAL archiving, your real RPO is the age of the newest file in the archive. The WAL, the write-ahead log, is written in sixteen megabyte files.
  15. NadiaAt the evening peak one fills in seconds. At four in the morning it fills slowly, so a setting called archive timeout forces a new file at least once a minute.
  16. ImranAnd if archiving breaks?
  17. NadiaThen the RPO grows every minute until somebody notices. That's why archive age is an alert. We'll come back to that.
  18. ImranAnd the RTO is just how long the restore command runs?
  19. NadiaThat's the trap. The RTO is every step added together. Someone notices and decides to restore. You fetch the base backup. You replay the WAL. You check the data. You switch the app over.
  20. NadiaIf you can see your screen, the first diagram shows our first drill as a row of bars, one step after another. If not, picture five steps. Twenty minutes to notice and decide.
  21. NadiaFifty-five minutes to fetch eight hundred gigabytes. Then one huge bar: a hundred and forty minutes of WAL replay. Then twenty minutes of checks and fifteen to switch the app.
  22. ImranWhy is replay so slow? The database only grows about one and a half gigabytes a day.
  23. NadiaGood question. It grows that much, but it writes far more WAL, roughly thirty gigabytes on a normal day. Every update, every index change, and the first change to each page after a checkpoint all go into the log.
  24. NadiaOur full backups ran on Sundays, so a restore to Monday evening meant replaying about six days of it, one change after another.
  25. ImranSo the fix is a faster network?
  26. NadiaThat helps a little. The big win was a smaller base backup every night, so replay never covers more than a day. The backup schedule sets your restore time as much as the restore tool does.
  27. ImranLet's talk about pg dump. Developers love it. Is it a real backup?
  28. NadiaIt's a good second kind of backup. It can restore one table, and it can move data to a newer Postgres version. But a dump of one database leaves things out.
  29. NadiaRoles and passwords belong to the whole server, so they're not in it. You dump those separately with pg dumpall and the globals only option, and you restore that file first.
  30. ImranOr else?
  31. NadiaOr else every grant fails with role does not exist, and the app can't log in. That happened in our drill. The config files aren't in any dump either, so keep a copy next to it.
  32. ImranAnything else a dump does that I should know about?
  33. NadiaTwo things that bite in production. A dump reads everything inside one long transaction, so for the two or three hours it runs, vacuum can't clean up dead rows.
  34. NadiaAnd it holds a lock on every table, so a schema migration that runs at the same time has to wait, and everything queues behind the migration. Never schedule a big dump and a migration together.
  35. ImranNow the physical side. Base backups and the WAL archive.
  36. NadiaHere's the part people miss. Archiving is a contract. When a WAL file is full, Postgres marks it ready and runs your archive command. Your command must return success only when the file is truly safe in storage.
  37. NadiaThe second diagram shows both paths. If you can't see it, imagine a fork in the road. On success, the file is marked done and later recycled. On failure, Postgres keeps the file and tries the same one again, forever. It never skips a file, because one gap would break every restore after it.
  38. ImranForever sounds bad.
  39. NadiaPicture this. The storage bucket's password expires on Friday evening. Nothing breaks at first. But WAL piles up on the primary, about thirty gigabytes a day, until the disk is full. Then the primary stops accepting writes. A backup problem has taken production down.
  40. ImranCan't you delete the old WAL files to make room?
  41. NadiaThat's the tempting wrong move. Those files are there because they haven't been archived. Delete them and no restore can ever pass that point. Fix the archive command instead and let the backlog drain by itself.
  42. ImranOkay. Say someone runs a bad update at nineteen forty. How do I pick what to restore?
  43. NadiaThree choices, before you touch any server. First, the base backup. Take the newest good one that finished before the target. If a backup started at nineteen twenty and finished at nineteen fifty-eight, you can't use it, because it wasn't consistent yet at nineteen forty.
  44. NadiaSecond, the WAL range. You need every file from where that backup started to the one that contains your target, with no gaps. Check that first. Finding a missing file two hours into replay costs you the whole restore.
  45. NadiaThird, the target. Usually a time. But there's a lovely habit: before a risky migration, you create a named restore point. Then if the migration goes wrong, recovery can stop exactly there, by name.
  46. ImranAnd I restore over production?
  47. NadiaNever. Restore a copy on a spare server, with archiving turned off on the copy, so it doesn't push a second history into the production archive. Then copy the good rows back into production while it keeps taking orders.
  48. ImranTell me how our own drill went, start to finish.
  49. NadiaKen expected an hour. The clock said four hours ten. The drill also found that the restored server had no password for the app's role, because nobody stored the globals dump next to the base backups.
  50. NadiaSo three changes. A differential backup every night. More parallel processes for the restore, which cut the fetch from fifty-five minutes to about thirty-five. And a globals dump plus the config files stored with every backup. The October drill measured one hour forty-five. Inside the target.
  51. ImranAnd now when finance asks, I can say one hour forty-five, tested on the fourteenth of October.
  52. NadiaWhich is worth far more than a guess.
  53. ImranWhat should we actually watch day to day?
  54. NadiaFour signals. Archive age, from a view called pg stat archiver: alert when it passes the RPO. Archiving failing right now, meaning the last failure is newer than the last success.
  55. NadiaThe backlog of files waiting to be archived. And the age of the newest base backup, alerting above about twenty-six hours.
  56. ImranWhy not just alert on the failed count?
  57. NadiaBecause a failure from last week that has since recovered still counts. And alert on silence too. A backup job that never starts prints no error at all.
  58. ImranWalk me through the Build it section.
  59. NadiaFive steps. You take a logical backup properly: the globals file, a parallel dump, a copy of the config, and a test restore in that order.
  60. NadiaThen you set up pgBackRest with two repositories, one in the same region for fast restores and one in another region and account. Full backups on Sunday, differentials every other night.
  61. NadiaThen a monitoring query for archive health, plus a check on the newest backup's age. Then a restore point before risky migrations.
  62. NadiaAnd finally a drill script that restores yesterday at seven thirty in the evening onto a scratch server, times it, checks the newest order, and writes the result to a log.
  63. ImranAny trap in that script?
  64. NadiaA big one. A server in recovery accepts read-only connections long before replay reaches the target. If your script stops the clock when the server answers, it reports a fast restore that isn't real. Wait until the server says it's no longer in recovery.
  65. ImranAnd in the practice section?
  66. NadiaTwo SQL exercises. In the first, you read a backup catalog and a WAL archive and pick the backup and WAL range for a restore. Watch for a backup that finished too late, and count the missing files. In the second, you flag backup problems across all our database servers.
  67. ImranFor my next review, what do I ask?
  68. NadiaWhen did we last restore the order database onto an empty server, and how long did each step take? If archiving failed at six on a Friday evening, which alert fires, and how long before the disk fills?
  69. NadiaAnd where are the globals dump, the config files and the encryption password, and can the on-call engineer reach them alone?
  70. NadiaHere's one to think about. We move to a managed database with seven days of automatic point-in-time recovery. Is restoring still partly our job? Take a second.
  71. ImranI'd guess no. That's what we'd be paying for.
  72. NadiaPartly yes. The provider takes the backups and archives the WAL. But we still choose the retention, turn on a copy in another region if we need one, and time a restore, because nobody promises how long eight hundred gigabytes takes. And the restore lands on a new instance with a new address.
  73. ImranLet me sum up. One: measure. RPO is the archive's age, and RTO is every step added up, with WAL replay often the biggest. Two: a dump needs the roles and config files next to it. Three: run a restore drill every month, timed and written down.
  74. NadiaGood. One addition. When archiving fails, the primary keeps the WAL and the disk fills, so never delete those files. Fix the command.
  75. ImranNext time?
  76. NadiaReplication and failover. What happens in the seconds after the primary dies, and how to make sure only one server thinks it's in charge.
Finished reading?Mark it complete to fill in its stop on your course line.

Use it at work

Runbook

Symptoms, first checks with commands, fixes and when to escalate, for one alert.

Open in the DevOps & TechOps Lab
Next stopReplication and failover
Was this lesson helpful?
Questions and discussion

Loading the discussion…