01RPO & RTO — the two dials of disaster recovery★ start here▶
Scenario: the database is gone. Your boss asks two questions: "how much data did we lose?" and "how long until we're back?" Every DR decision — backup frequency, replica placement, budget — is really an answer to those two numbers. Name them first, then build to hit them.
What
RPO (Recovery Point Objective) = how much data you can lose, in time. RTO (Recovery Time Objective) = how long you can be down. Two targets, measured in minutes/hours.
Why
They turn "don't lose data" into a design and a budget. Tighter RPO/RTO cost more; the business decides the number, engineering builds to it.
How
RPO is set by backup/replication frequency; RTO by how fast you can restore or promote. Pick per system — a ledger ≠ an analytics cache.
last good copy DISASTER back online
───────────────●───────────────────────╳───────────────────●──────────►
\_______ RPO _______/ \_______ RTO _______/
data written here downtime here
is LOST on recovery until service returns
nightly backup → RPO up to 24h restore 2TB from S3 → RTO hours
WAL every 1 min → RPO ~1 min promote hot standby → RTO seconds
sync replica → RPO ~0 (choose per system & per budget)# RPO — how much DATA you can afford to lose (measured in time) # nightly logical dump only → up to 24h of writes gone # WAL archived every 1 min → ~1 min of writes at risk # synchronous standby → ~0 (no acknowledged write is lost) # # RTO — how long you can be DOWN before recovery # restore a big backup from S3 → minutes to hours (size-dependent) # promote a warm/hot standby → seconds to a few minutes # # write the two numbers per system FIRST; they pick every other decision.
✅ Do
- Write an explicit RPO and RTO for each system before designing DR
- Let the business own the numbers — they imply cost and complexity
- Re-derive backup frequency and replica strategy from the targets
❌ Don't
- Say "we can't lose any data" for everything — near-zero RPO is expensive
- Confuse RPO (data loss) with RTO (downtime) — they need different controls
02What you're recovering from — the failure taxonomy▶
Scenario: you built a beautiful multi-AZ setup and feel safe — then an engineer runs DELETE FROM orders without a WHERE, and your standby faithfully replicates the deletion in milliseconds. High availability didn't help, because you were defending the wrong failure. Different disasters need different defenses.
What
The categories of disaster: hardware/AZ failure, region outage, data corruption, human error (bad DELETE/migration), ransomware/malice, and accidental deletion of the DB itself.
Why
Each needs a different control. Replication survives a dead server but happily copies a bad DELETE. Only point-in-time backups undo human error.
How
Map each failure to a defense: HA for hardware, cross-region for region loss, PITR/immutable backups for human error and ransomware.
FAILURE WHAT SAVES YOU ───────────────────────── ─────────────────────────────── server/disk dies Multi-AZ standby (HA) t13 whole AWS region down cross-region replica/backup t14,17,18 bad DELETE / migration Point-In-Time Recovery t8,15 data corruption spreads PITR to BEFORE the corruption ransomware / malicious wipe immutable, cross-account copies t18 someone drops the database backups that live ELSEWHERE → replication does NOT fix the bottom four — it copies them faithfully
# human error, replicated in milliseconds to every standby: $ psql -c "DELETE FROM orders;" # forgot the WHERE clause DELETE 4821993 # ...and the hot standby now agrees # HA (multi-AZ) gives you a perfect copy of the WRONG state. # only a copy from BEFORE the command saves you: $ # restore to 10:41:59, one second before the delete (PITR, topic 8) $ # recovery_target_time = '2026-07-14 10:41:59+00'
✅ Do
- List the failure modes and map each to a specific control
- Keep point-in-time backups to undo human error and corruption
- Store at least one copy immutably and in another account/region
❌ Don't
- Assume replication or Multi-AZ is "backup" — it copies mistakes instantly
- Defend only the dramatic failures (region loss) and skip the common one (a bad DELETE)
us-east-1 — it's a human running a bad migration or an app deleting rows it shouldn't. These "logical" failures replicate instantly to every standby and every synchronous replica, so the fancier your HA, the faster the mistake spreads. The only defense is a backup that captures a point before the error — which is why PITR (topic 8) is the backbone of real DR.03Backup vs replication vs DR — three different jobs★ core idea▶
Scenario: "We have a replica, so we're backed up." No. A replica gives you a hot copy for failover, but if a row is corrupted or deleted, the replica has the corruption too. Backup, replication, and DR are three distinct capabilities — and you need all three, because each fails where the others cover.
What
Backup = a point-in-time copy you can restore from the past. Replication = a live, up-to-date copy for availability. DR = the whole plan (people, steps, targets) to recover from disaster.
Why
Confusing them is the classic fatal mistake. Replication protects against a dead server; backups protect against a bad write; DR is what ties strategy, testing, and runbooks together.
How
Run all three: streaming replicas for HA/RTO, PITR backups for RPO and human-error recovery, and a written, tested DR plan over both.
BACKUP REPLICATION DR (the plan) a copy from a live copy for strategy + runbook + the PAST NOW / failover testing over both ─────────── ─────────────── ───────────────────── undo a mistake survive a crash hit RPO & RTO on purpose restore in time promote in seconds people, comms, drills protects RPO protects RTO protects the BUSINESS a replica is NOT a backup (it copies corruption) a backup is NOT HA (restoring takes time) neither is DR without a tested plan wrapping them
# REPLICATION: check the live standby is caught up (availability) $ psql -c "SELECT client_addr, state, replay_lag FROM pg_stat_replication;" # → survives a primary crash; does NOT let you go back in time # BACKUP: restore yesterday's state (recover from a mistake) $ pg_restore -d shop_restored /backups/shop_2026-07-13.dump # → undoes a bad DELETE; does NOT give instant failover # DR: the plan that says WHICH to use, WHO runs it, and the target $ cat dr-runbook.md # RPO/RTO, promote steps, comms, contacts (topic 24)
✅ Do
- Run replication for availability and backups for recoverability
- Wrap both in a written, tested DR plan with owners and targets
- Say precisely which capability protects which failure
❌ Don't
- Call a replica a backup — it faithfully replicates corruption and deletes
- Assume having backups means you have DR — DR is the tested plan around them
DROP TABLE, a corrupting bug, or ransomware reaches the standby as fast as the network allows. Backups are the only copy that exists at a different point in time, which is exactly what you need to recover from anything logical. Keep them separate, and keep at least one backup somewhere the primary's credentials can't reach.04The DR strategy spectrum — from restore to active-active▶
Scenario: two systems, two very different needs. Your internal wiki can be down for hours and lose a day — a nightly backup is plenty. Your payments ledger can lose nothing and must fail over in seconds. Same company, opposite DR strategies. AWS names four points on this spectrum; you pick per system.
What
Four DR patterns, cheapest/slowest to priciest/fastest: Backup & Restore → Pilot Light → Warm Standby → Multi-Site Active-Active.
Why
Cost and complexity climb as RTO/RPO shrink. Matching the pattern to each system's targets avoids both under-protecting the ledger and overspending on the wiki.
How
Pick per workload from its RPO/RTO: hours-and-a-day → restore; minutes → warm standby; near-zero → active-active (and a big bill).
BACKUP & RESTORE PILOT LIGHT WARM STANDBY ACTIVE-ACTIVE ───────────────── ────────────── ─────────────── ────────────── backups in S3; data replicated; scaled-down full full stacks in spin up on disaster core off till need running copy 2+ regions, live RTO: hours RTO: 10s of min RTO: minutes RTO: ~0 RPO: hours RPO: minutes RPO: seconds RPO: ~0 $ $$ $$$ $$$$ wiki / analytics typical apps important apps the ledger
# internal wiki: RTO hours, RPO 1 day → BACKUP & RESTORE # nightly RDS snapshot; on disaster, restore into the DR region # customer app: RTO minutes, RPO seconds → WARM STANDBY # cross-region read replica kept warm; promote + repoint on disaster # payments ledger: RTO ~0, RPO ~0 → ACTIVE-ACTIVE # Aurora Global Database (topic 17) or multi-region writers; expensive # # one company runs ALL of these at once — matched to each system's targets.
✅ Do
- Pick a pattern per system from its RPO/RTO, not one size for all
- Start cheap; move up the spectrum only where the targets demand it
- Cost the higher tiers honestly — active-active roughly doubles infra
❌ Don't
- Build active-active for a system that can tolerate a nightly restore
- Put the revenue-critical ledger on backup-and-restore to save money
05Durability & the 3-2-1 backup rule▶
Scenario: your backups run nightly to a folder on the same server as the database. The server's disk fails — and takes the database and every backup with it. Or the AWS account is compromised and the attacker deletes the DB and its snapshots in the same breath. A backup that shares fate with the thing it protects isn't a backup.
What
The 3-2-1 rule: keep 3 copies of data, on 2 different media/systems, with 1 off-site (different region/account) — ideally one immutable.
Why
Backups fail together when they share fate — same disk, same account, same region, same credentials. Independence is what makes a copy survive the disaster that killed the original.
How
Automated backups + a copy in another region + a copy in another account (or object-lock/immutable), so no single failure or actor can take them all.
● primary DB (region A, account A)
● backup #2 (region A, account A) ← same account: attacker deletes both
● backup #3 (region B, account B, object-lock/immutable) ← survives
3 copies · 2 systems (RDS + S3) · 1 off-site & immutable
test: "could ONE failure or ONE compromised credential take them all?"
if yes → they share fate → it's not 3-2-1 yet# copy #2: automated backups in the primary region/account
$ aws rds describe-db-snapshots --db-instance-identifier shop-prod
# copy #3: replicate to a DIFFERENT region AND account, immutably
$ aws rds copy-db-snapshot \
--source-db-snapshot-identifier shop-prod-2026-07-14 \
--target-db-snapshot-identifier shop-dr-2026-07-14 \
--kms-key-id alias/dr-key --region us-west-2 # cross-region
# + S3 backups with Object Lock (WORM) in a locked-down backup account
$ aws s3api put-object-lock-configuration --bucket pg-dr-backups ... # immutable✅ Do
- Keep at least one backup in a different region and account
- Make one copy immutable (S3 Object Lock / vault lock) against ransomware
- Ask "could one failure or one credential delete them all?" — fix it if yes
❌ Don't
- Store backups on the same host/disk/account as the database
- Give the app's or admin's normal credentials delete rights on backups
06Logical backups — pg_dump & pg_restore▶
Scenario: you need to copy one database to a staging environment, or restore a single accidentally-dropped table — not the whole cluster. A logical backup exports the data as SQL/objects you can restore selectively, into any compatible Postgres. It's portable and surgical, but it's slow to make and restore at scale.
What
pg_dump exports one database as a logical set of SQL statements or a compressed archive; pg_dumpall adds cluster-wide objects (roles, tablespaces). pg_restore loads an archive back.
Why
It's portable (across versions/platforms) and selective (one table, one schema) — ideal for migrations, dev refreshes, and single-object recovery.
How
Use the custom/directory format (-Fc/-Fd) for compression and parallel restore; it's a consistent snapshot at dump-start time.
running DB ──pg_dump -Fc──► shop.dump (compressed archive)
│ contains: schema + data as objects
▼
fresh DB ◄──pg_restore -j 4───┘ (parallel, selective: -t one_table)
✓ portable across versions/OS ✗ slow to dump/restore huge DBs
✓ restore ONE table ✗ RPO limited to dump frequency
✓ human-diffable (plain format) ✗ not point-in-time (topic 8 is)# ✅ custom format: compressed, restore in parallel, restore ONE object $ pg_dump -Fc -d shop -f shop.dump # consistent as of dump start $ pg_restore -d shop_new -j 4 shop.dump # parallel restore # restore just one accidentally-dropped table (surgical recovery) $ pg_restore -d shop -t orders --data-only shop.dump # cluster-wide roles/grants live outside pg_dump — capture them too $ pg_dumpall --globals-only > globals.sql # roles, tablespaces # ❌ pg_dump shop > shop.sql # plain SQL: no parallelism
✅ Do
- Use
-Fc(custom) or-Fd(directory) for compression and parallel restore - Capture globals separately with
pg_dumpall --globals-only - Use logical dumps for migrations, dev refreshes, and single-object recovery
❌ Don't
- Rely on nightly
pg_dumpalone for a large, low-RPO production DB - Forget roles/grants — a data-only restore into an empty cluster fails without them
pg_dump is consistent as of the moment it started, and on a large busy database it can run for hours holding that snapshot — so your real RPO is "last night's dump," and restore time (RTO) grows with data size. For anything with a tight RPO/RTO, logical dumps are a supplement (portability, selective restore), not the backbone. The backbone is physical backups + WAL (topics 7–9).07Physical backups & the WAL▶
Scenario: your database is 2TB and needs a tight RPO. A nightly logical dump can't keep up. You need a physical backup — a byte-level copy of the data files — plus the Write-Ahead Log, the stream of every change, which together let you restore fast and roll forward to any second. This pairing is the heart of serious Postgres DR.
What
A physical (base) backup is a copy of the actual data directory (via pg_basebackup). The WAL (Write-Ahead Log) records every change before it's applied — the redo stream.
Why
Base backup + archived WAL = fast restore and the ability to replay forward to any point (PITR, topic 8). It's how replication and low-RPO recovery both work.
How
Take a base backup, then continuously archive WAL segments. Restore = lay down the base, then replay WAL up to your target time.
BASE BACKUP (byte copy of data dir) WAL stream (every change, in order) ┌───────────────────────────┐ w1 w2 w3 w4 w5 w6 w7 w8 ... │ full snapshot @ 02:00 │ ─────► ───────────────────────────► └───────────────────────────┘ each segment archived off-box restore = copy base @02:00, then REPLAY WAL forward to your target (WAL is also what a streaming replica consumes live, topic 10) lose the WAL and you're stuck at the base backup's moment.
# physical base backup — byte-level copy of the data directory $ pg_basebackup -h primary -D /backup/base -Fp -Xs -P -R # -Xs stream WAL alongside · -R writes standby config · -P progress # WAL is written continuously; archive each segment somewhere durable: # postgresql.conf # wal_level = replica # archive_mode = on # archive_command = 'aws s3 cp %p s3://pg-wal/%f' # ship each segment # # restore later = restore base, then set a restore_command to fetch WAL # restore_command = 'aws s3 cp s3://pg-wal/%f %p' # replay forward
✅ Do
- Base backup + continuous WAL archiving for large/low-RPO databases
- Archive WAL to durable, off-box storage (S3) — the WAL is half your recovery
- Prefer a tool (pgBackRest/WAL-G, topic 9) over hand-rolled archive scripts
❌ Don't
- Keep WAL only on the primary's disk — lose the disk, lose the roll-forward
- Assume a base backup alone is enough — without WAL you're frozen at its timestamp
archive_command fails silently (S3 perms, full disk) and nobody's watching, WAL segments pile up on the primary — filling its disk and, worse, leaving a gap so you can only recover to the last successfully-archived segment. Monitor archiving success and pg_wal disk usage as first-class DR health signals; a broken archiver quietly destroys your RPO.08Point-in-time recovery (PITR)★ core idea▶
Scenario: at 10:42 someone ran a bad migration that corrupted the orders table. You don't want last night's backup (you'd lose a day) and you can't use the replica (it copied the corruption). You want the database exactly as it was at 10:41:59 — one second before. That's point-in-time recovery, and it's the single most important DR capability.
What
PITR = restore a base backup, then replay archived WAL forward and stop at a chosen time (or transaction), landing the database at that exact moment.
Why
It's the only way to undo logical disasters — bad writes, migrations, corruption — that replication faithfully copies. It converts "we lost a day" into "we lost one second."
How
Set recovery_target_time (or LSN/xid), point restore_command at your WAL archive, and start recovery; Postgres replays up to the target and stops.
base @02:00 ──replay WAL──► ... 10:41:58 10:41:59 │ 10:42:00 BAD MIGRATION
▲
recovery_target_time = '10:41:59' ← STOP here
result: a database identical to the instant BEFORE the corruption.
(choose target by time, by transaction id, or by a named restore point)# 1) restore the most recent base backup taken BEFORE the target time $ cp -a /backup/base/* $PGDATA/ # 2) tell Postgres where to fetch WAL and WHEN to stop # postgresql.conf / recovery settings: # restore_command = 'aws s3 cp s3://pg-wal/%f %p' # recovery_target_time = '2026-07-14 10:41:59+00' # recovery_target_action = 'promote' $ touch $PGDATA/recovery.signal # PG 12+: signals recovery mode $ pg_ctl start # replays WAL up to 10:41:59, then promotes # verify you landed before the damage, THEN repoint the app $ psql -c "SELECT count(*) FROM orders;" # sanity-check the row count
✅ Do
- Keep base backups + continuous WAL so any recent second is reachable
- Restore to just before the bad event; verify before repointing the app
- Know your target options: time, transaction id (xid), or named restore point
❌ Don't
- Recover onto the damaged instance — restore to a NEW one, then cut over
- Guess the target time — use logs/audit to find the exact moment of harm
09Continuous WAL archiving — pgBackRest & WAL-G▶
Scenario: you tried hand-rolling archive_command = 'cp %p /mnt/wal/%f' and a dozen things went wrong — a full disk left a gap, no compression blew your storage budget, and you had no easy way to verify or expire old backups. Purpose-built backup tools solve exactly these, which is why nobody runs serious Postgres DR on shell scripts.
What
Backup managers (pgBackRest, WAL-G) that automate base backups + WAL archiving to object storage, with compression, encryption, parallelism, verification, and retention.
Why
They turn fragile hand-written archiving into a reliable system — atomic backups, integrity checks, incremental/differential backups, and one-command restore/PITR.
How
Configure a repository (S3), set archive_command to the tool, schedule full/incremental backups, and let it manage expiry to your retention policy.
primary ──archive_command = pgbackrest──► S3 repo (compressed, encrypted)
│ full / differential / incremental backups on a schedule
│ ├─ integrity verified (checksums)
│ ├─ retention: keep 2 full + WAL to satisfy PITR window
└─ restore: `pgbackrest restore --type=time --target=...` (one command)
vs. archive_command = 'cp %p /mnt/wal/%f'
→ no compression, no verify, no retention, silent gaps on full diskcp is your arms.# postgresql.conf — let the tool own archiving
# archive_command = 'pgbackrest --stanza=shop archive-push %p'
# take backups on a schedule (full weekly, incremental daily)
$ pgbackrest --stanza=shop --type=full backup
$ pgbackrest --stanza=shop --type=incr backup
$ pgbackrest --stanza=shop check # verify archiving + backups are healthy
# point-in-time restore — the tool fetches the right base + WAL for you
$ pgbackrest --stanza=shop --type=time \
--target="2026-07-14 10:41:59+00" restore
# WAL-G is a similar tool (Go, cloud-native): wal-g backup-push / backup-fetch✅ Do
- Use pgBackRest or WAL-G to object storage with compression + encryption
- Run its
check/verify regularly and alert on failures - Set retention so WAL and base backups jointly cover your PITR window
❌ Don't
- Hand-roll
archive_commandwithcp/scpfor production - Skip the periodic verify — an unverified backup pipeline fails silently
check/verify and never test a restore. pgBackRest can be dutifully pushing WAL to a bucket the restore role can't read, or expiring WAL your retention math got wrong — and you won't know until a real recovery. The tool automates the mechanics; it does not automate confidence. That still comes from a scheduled, timed restore drill (topic 11).10Streaming replication & replicas▶
Scenario: your primary's server dies at 3am. With only backups, you're restoring for an hour (bad RTO). With a streaming replica already caught up, you promote it in seconds and you're back — that's the availability half of DR. But choose sync vs async carefully: it's the difference between "zero data loss" and "fast, but maybe lose the last few writes."
What
A standby continuously replays the primary's WAL stream, staying a hot, near-live copy. Async = fast, may lag; sync = primary waits for the standby to confirm (zero data loss, slower writes).
Why
Replicas give you low RTO (promote in seconds) and read scaling. They're the availability backbone — but not a substitute for backups (they copy mistakes).
How
Stand up a standby from a base backup, stream WAL, monitor lag. On failure, promote it to primary and repoint apps (topics 20–21).
PRIMARY ──WAL stream──► STANDBY (replays continuously, ready to promote)
│ │
ASYNC: primary acks the client FIRST, ships WAL after
→ fast writes, but a crash can lose the last few txns (RPO > 0)
SYNC: primary WAITS for standby to confirm, THEN acks client
→ zero data loss on failover (RPO 0), but slower writes
promote standby → new primary in seconds (great RTO); still keep backups!# create a standby from the primary (writes standby config with -R)
$ pg_basebackup -h primary -D /var/lib/pgsql/data -Fp -Xs -R -P
# on the primary, require a synchronous standby for zero-data-loss writes
# postgresql.conf: synchronous_standby_names = 'standby1'
# watch replication health & lag (this lag is your async RPO, topic 22)
$ psql -c "SELECT client_addr, state, sync_state,
write_lag, replay_lag FROM pg_stat_replication;"
# on primary failure, promote the standby to a read/write primary
$ pg_ctl promote # or: SELECT pg_promote(); → new primary in seconds✅ Do
- Run standbys for low RTO; monitor
replay_lagcontinuously - Use synchronous replication where RPO must be zero (weigh the write cost)
- Keep backups too — replicas don't protect against logical errors
❌ Don't
- Treat a replica as your backup — it replicates corruption and deletes
- Run a single synchronous standby with no fallback — if it stalls, writes block
ANY 1 (s1, s2, s3)) so one slow replica can't freeze production. Sync replication protects RPO but must be designed so it doesn't sink your RTO.11Restore drills — testing that a backup actually restores★ do this▶
Scenario: the outage is real, you reach for the backups, and… the restore fails — a missing WAL segment, an unreadable bucket, a wrong key, a corrupt archive. Backups had been "running green" for a year, but nobody ever restored one. An untested backup isn't a backup; it's a hope. The only proof is a restore you actually performed and timed.
What
A restore drill: regularly restore your backups into a scratch environment, verify the data, and time it — turning assumptions into a measured RTO and proven recoverability.
Why
Backups fail in ways only a restore reveals: gaps, bad perms, wrong keys, corrupt files, forgotten globals. And your real RTO is whatever the drill measured, not the slide.
How
Automate a periodic restore-and-validate: fetch the backup, restore to a target time, run integrity checks, record the elapsed time, alert on failure.
backup job ✅✅✅✅✅ (a year of green checkmarks)
│
▼ but has anyone RESTORED one?
restore drill: fetch → restore to target time → VALIDATE → time it
├─ row counts / checksums match? → recoverable ✓
├─ how long did it take? → your REAL RTO
└─ anything missing (WAL, globals)? → fix BEFORE the real outage
schedule it (weekly/monthly) & alert if it fails — like any other test.# scheduled DR drill (pseudo) — run monthly, alert on any failure
$ START=$(date +%s)
$ pgbackrest --stanza=shop --type=time \
--target="$(date -d '1 hour ago' '+%Y-%m-%d %H:%M:%S+00')" \
--pg1-path=/scratch/restore restore # restore into a SCRATCH cluster
$ pg_ctl -D /scratch/restore start
$ psql -p 5433 -c "SELECT count(*) FROM orders;" # validate: sane data?
$ psql -p 5433 -c "SELECT pg_is_in_recovery();" # promoted correctly?
$ echo "RTO this drill: $(( $(date +%s) - START ))s" # record REAL RTO
# fail the job (page on-call) if any step errors — a broken restore is an incident✅ Do
- Schedule automated restore-and-validate drills (weekly/monthly)
- Record the elapsed time — that's your real, defensible RTO
- Validate the data (row counts, checksums), not just that the process ran
❌ Don't
- Trust "backup job succeeded" as proof you can recover
- Only ever test-restore during an actual disaster
12RDS automated backups & manual snapshots▶
Scenario: you're on RDS and assume "AWS handles backups." Partly true — but the defaults may keep only 7 days, automated backups are deleted when you delete the instance, and they don't leave the region. Knowing exactly what RDS backs up, for how long, and where, is the difference between a smooth restore and a nasty surprise.
What
RDS offers automated backups (daily snapshot + 5-min transaction logs → enables PITR, kept for a retention window) and manual snapshots (you take them; they persist until you delete them).
Why
Automated backups power RDS point-in-time restore; manual snapshots survive instance deletion and can be copied cross-region/account. You need both, configured deliberately.
How
Set a retention period that covers your PITR window, take manual snapshots before risky changes, and copy critical snapshots out of the region/account.
AUTOMATED BACKUPS MANUAL SNAPSHOTS ────────────────────────── ──────────────────────────── daily snapshot + 5-min tx logs on-demand, point-in-time image → enables PITR within retention → kept until YOU delete them retention: 0–35 days (default 7) → copyable cross-region/account DELETED when you delete the DB! → survive instance deletion set retention ≥ your PITR need · take a manual snap before migrations · copy the important ones out of region+account (topic 18)
# set automated-backup retention to cover your PITR window
$ aws rds modify-db-instance --db-instance-identifier shop-prod \
--backup-retention-period 14 --apply-immediately # 0 = DISABLED!
# take a durable manual snapshot before a risky migration
$ aws rds create-db-snapshot --db-instance-identifier shop-prod \
--db-snapshot-identifier shop-prod-premigration-2026-07-15
# what point-in-time can we restore to right now?
$ aws rds describe-db-instances --db-instance-identifier shop-prod \
--query 'DBInstances[0].LatestRestorableTime' # your current RPO edge✅ Do
- Set backup retention to cover your required PITR window (up to 35 days)
- Take a manual snapshot before migrations and major changes
- Copy critical manual snapshots to another region and account (topic 18)
❌ Don't
- Leave retention at 0 (disabled) or assume the default covers your RPO
- Forget that deleting the instance deletes its automated backups
13RDS Multi-AZ — high availability, not disaster recovery★ misunderstood▶
Scenario: "We're Multi-AZ, so we're covered for DR." This is the most common and most dangerous RDS misconception. Multi-AZ gives you automatic failover within one region if a server or AZ fails — genuinely useful — but it is not a backup, does not protect a whole-region outage, and replicates a bad DELETE to the standby instantly.
What
Multi-AZ keeps a synchronous standby in a second Availability Zone of the same region and fails over automatically (usually 60–120s) if the primary or its AZ dies.
Why
It's excellent HA (survives hardware/AZ failure with low RTO) — but people mistake it for DR. It doesn't cover region loss, human error, corruption, or accidental deletion.
How
Use it for availability, and layer real DR on top: cross-region snapshot copies/replicas for region loss, PITR/backups for logical failures.
REGION us-east-1 ┌──────────────────────────────────────────┐ │ AZ-a primary ══sync══► AZ-b standby │ auto-failover in ~60–120s └──────────────────────────────────────────┘ ✅ survives: server crash, AZ outage (great HA, low RTO) ❌ region us-east-1 goes down → standby is in the SAME region, also down ❌ bad DELETE / migration → replicated to the standby instantly ❌ you delete the DB → both gone; it's not a backup Multi-AZ = availability. DR still needs cross-region + PITR.
# enable Multi-AZ (HA within the region) — good, but not DR
$ aws rds modify-db-instance --db-instance-identifier shop-prod \
--multi-az --apply-immediately
# DR still requires, ON TOP of Multi-AZ:
# • a cross-region read replica or copied snapshots (region loss) → t14,18
# • automated backups / PITR (bad writes, corruption) → t15
# • an immutable, cross-account copy (ransomware, deletion) → t5,18
$ aws rds describe-db-instances --db-instance-identifier shop-prod \
--query 'DBInstances[0].MultiAZ' # true ≠ "we have DR"✅ Do
- Use Multi-AZ for in-region high availability and low failover RTO
- Layer cross-region copies and PITR on top for actual DR
- Say it out loud in reviews: "Multi-AZ is HA, not our DR plan"
❌ Don't
- Treat Multi-AZ as a backup or as region-failure protection
- Assume the sync standby saves you from a bad write — it copies it instantly
14Read replicas & cross-region replicas▶
Scenario: a regional outage is your nightmare, and Multi-AZ won't save you. A cross-region read replica keeps a continuously-updated copy of your database in a different region, ready to promote into a standalone primary — turning "the region is gone" from an extinction event into a promote-and-repoint operation with minutes of RTO.
What
An RDS read replica asynchronously replicates from the primary. Placed in another region, it's a warm DR copy you can promote to a full read/write primary during a regional disaster.
Why
It's the practical answer to region loss for RDS: a live copy outside the blast radius. It also offloads reads and can seed a new region.
How
Create a cross-region replica, monitor its lag (your RPO), and on disaster promote it (it becomes independent) and repoint applications.
us-east-1 (primary) ──async replication──► us-west-2 (read replica)
│ region outage ✗ │ survives — different region
│ ▼
└───────────────────────────► promote replica → new PRIMARY (r/w)
then repoint apps (topic 21)
RPO = the replica's lag (async → seconds, watch it, topic 22)
RTO = promotion time + app cutover (minutes) — much better than restore# create a CROSS-REGION read replica (warm DR copy)
$ aws rds create-db-instance-read-replica \
--db-instance-identifier shop-dr-west \
--source-db-instance-identifier arn:aws:rds:us-east-1:...:db:shop-prod \
--region us-west-2
# monitor replica lag — this IS your RPO for a regional failover (topic 22)
$ aws cloudwatch get-metric-statistics --namespace AWS/RDS \
--metric-name ReplicaLag --dimensions Name=DBInstanceIdentifier,Value=shop-dr-west ...
# DISASTER: promote the replica to a standalone read/write primary
$ aws rds promote-read-replica --db-instance-identifier shop-dr-west
# → now independent; repoint apps to the new endpoint (topic 21)✅ Do
- Keep a cross-region read replica for region-loss recovery
- Monitor its replication lag — that lag is your regional RPO
- Rehearse the promote + app-cutover so the real one is muscle memory
❌ Don't
- Rely on same-region replicas for region-failure DR
- Forget that promotion is one-way — a promoted replica won't rejoin the old primary
15RDS point-in-time restore & snapshot restore▶
Scenario: a bad deploy corrupted data at 14:07. On RDS you don't hand-replay WAL — you click (or call) restore to point in time, name 14:06, and AWS spins up a brand-new instance at that moment from the automated backups. Crucially, it restores to a new instance, so your damaged one is untouched while you validate.
What
RDS point-in-time restore creates a new instance at any second within the backup-retention window (using the daily snapshot + transaction logs). Snapshot restore creates one from a specific snapshot.
Why
It's the managed equivalent of PITR (topic 8) — the way you undo logical disasters on RDS — and it always lands in a new instance, preserving the original for forensics.
How
Restore to a new instance at the target time, validate the data, then cut the app over (rename or repoint). Never overwrite the original blindly.
shop-prod (damaged @14:07) ── stays up for forensics ──┐
│
restore-to-point-in-time --restore-time 14:06:00 ▼
└──► shop-restored (NEW instance, clean state) ── validate ──►
repoint app (t21)
within backup-retention window · new endpoint · original untouched
(snapshot-restore is the same idea, from a chosen snapshot)# restore to a NEW instance at the second before the damage
$ aws rds restore-db-instance-to-point-in-time \
--source-db-instance-identifier shop-prod \
--target-db-instance-identifier shop-restored \
--restore-time 2026-07-15T14:06:00Z # any time in the retention window
# (or restore from a specific snapshot)
$ aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier shop-restored \
--db-snapshot-identifier shop-prod-premigration-2026-07-15
# validate the restored instance, THEN repoint the app (rename endpoints)
$ psql -h shop-restored... -c "SELECT count(*) FROM orders;" # sane? cut over.✅ Do
- Restore to a new instance at just before the incident; validate before cutover
- Keep the damaged instance for forensics until you've confirmed recovery
- Know your window — you can only PITR within the backup-retention period
❌ Don't
- Expect an "in-place rewind" — RDS restores create a new instance + endpoint
- Forget to move parameter groups, security groups, and app connection strings
16Aurora — storage, continuous backup & Backtrack▶
Scenario: you move to Aurora PostgreSQL and the DR story changes. Storage is a distributed, self-healing layer replicated 6 ways across 3 AZs; backups are continuous to S3 with no performance hit; and some engines offer Backtrack to rewind the cluster in place in seconds. Aurora shifts much of the mechanics to AWS — but you still own the strategy.
What
Aurora separates compute from a shared distributed storage layer (6 copies / 3 AZs). Backups are continuous and automatic to S3; PITR is built in. Backtrack (where supported) rewinds the cluster to a recent time without a restore.
Why
Higher baseline durability and easier PITR, plus faster in-region recovery. Backtrack can undo a recent bad change in seconds — no new-instance restore needed.
How
Rely on continuous backup for PITR; use Backtrack for quick "oops" rewinds within its window; still copy backups cross-region/account for real DR.
Aurora cluster
compute (writer + readers)
│ writes log records
▼
┌──────────────────────────────────────────┐
│ distributed storage: 6 copies / 3 AZs │ self-healing, +continuous
│ continuous backup to S3 (no perf hit) │ backup → built-in PITR
└──────────────────────────────────────────┘
BACKTRACK: rewind the SAME cluster to e.g. 5 min ago in seconds
(no restore, no new instance) — undo a recent bad change fast# PITR is continuous on Aurora — restore to any time in the window
$ aws rds restore-db-cluster-to-point-in-time \
--db-cluster-identifier shop-restored \
--source-db-cluster-identifier shop-aurora \
--restore-to-time 2026-07-15T14:06:00Z
# BACKTRACK (Aurora, supported engines): rewind the SAME cluster in place
$ aws rds backtrack-db-cluster --db-cluster-identifier shop-aurora \
--backtrack-to 2026-07-15T14:06:00Z # seconds, no new instance
# check the window you can backtrack within:
$ aws rds describe-db-clusters --db-cluster-identifier shop-aurora \
--query 'DBClusters[0].BacktrackWindow'✅ Do
- Use Aurora's continuous backup for easy PITR; enable Backtrack where it fits
- Still export/copy snapshots cross-region and cross-account for real DR
- Know the Backtrack window — it's a short recent buffer, not full history
❌ Don't
- Assume Aurora's 6-way storage removes the need for cross-region DR
- Treat Backtrack as your backup — it only reaches back a limited window
17Aurora Global Database — cross-region DR▶
Scenario: your ledger needs to survive a full region loss with an RPO measured in seconds and an RTO of about a minute — tighter than an async read replica gives. Aurora Global Database replicates at the storage layer to secondary regions with typically sub-second lag, and lets you fail over to another region fast. This is the managed near-zero-RPO cross-region option.
What
An Aurora cluster spanning regions: one primary (read/write) and up to five secondary read-only regions, replicated via the storage layer with typically <1s lag. Any secondary can be promoted.
Why
It delivers low cross-region RPO (~seconds) and fast RTO (managed/planned failover ~1 min) — the strong-DR option for critical, region-resilient workloads.
How
Add secondary regions; reads serve locally. On regional disaster, fail over to a secondary (it becomes the writer) and repoint apps to the region's endpoint.
PRIMARY us-east-1 (writer)
│ storage-layer replication (typically <1s lag)
├──────────────► SECONDARY us-west-2 (read-only, promotable)
└──────────────► SECONDARY eu-west-1 (read-only, promotable)
region us-east-1 down → FAILOVER: promote a secondary to writer (~1 min)
RPO ~ seconds · RTO ~ minute · reads served locally in each region
the strong option when a cross-region async replica's RPO is too loose# create a global cluster and attach a secondary region
$ aws rds create-global-cluster --global-cluster-identifier shop-global \
--source-db-cluster-identifier arn:aws:rds:us-east-1:...:cluster:shop-aurora
$ aws rds create-db-cluster --region us-west-2 \
--db-cluster-identifier shop-aurora-west \
--global-cluster-identifier shop-global --engine aurora-postgresql
# DISASTER: promote the secondary region to be the new writer
$ aws rds failover-global-cluster --global-cluster-identifier shop-global \
--target-db-cluster-identifier arn:aws:rds:us-west-2:...:cluster:shop-aurora-west
# then repoint apps to the us-west-2 endpoint (topic 21)✅ Do
- Use Global Database for critical workloads needing seconds-RPO across regions
- Serve reads locally from secondaries; rehearse the regional failover
- Automate the app repoint to the promoted region's endpoint
❌ Don't
- Reach for it (and its cost) when an async replica's RPO is acceptable
- Forget it still needs point-in-time backups underneath for logical errors
18Cross-region & cross-account copies — isolating the blast radius▶
Scenario: an attacker (or a fat-fingered admin) with access to your production account deletes the database and every snapshot in one sweep. Everything in that account shared its fate. The copy that saves you is the one in a separate account and separate region, that production's credentials can't touch. Backups must escape the blast radius of the thing they protect.
What
Copying snapshots/backups to a different region (survives region loss) and a different account (survives account compromise/deletion), ideally immutable — the concrete implementation of 3-2-1 (topic 5).
Why
Same-account, same-region backups share fate with the primary. Independence is what makes a copy survive the disaster, insider, or ransomware that took the original.
How
Automate snapshot copy to a locked-down backup account in another region, with its own KMS key and tightly scoped, ideally write-only-then-immutable access.
PROD account / us-east-1
● database ← attacker/admin with prod access can delete
● snapshots ← ...and these too (same account = same fate)
BACKUP account / us-west-2 (different account AND region)
● copied snapshots ← separate credentials, own KMS key, immutable
└─ prod's blast radius does NOT reach here → survives
automate the copy; scope the backup account so prod can't touch it.# copy an RDS snapshot to another REGION (survives region loss)
$ aws rds copy-db-snapshot \
--source-db-snapshot-identifier arn:aws:rds:us-east-1:PROD:snapshot:shop-2026-07-15 \
--target-db-snapshot-identifier shop-dr-2026-07-15 \
--kms-key-id alias/dr-key --region us-west-2
# share + copy into a separate BACKUP ACCOUNT (survives account compromise)
$ aws rds modify-db-snapshot-attribute --db-snapshot-identifier shop-2026-07-15 \
--attribute-name restore --values-to-add 999988887777 # backup acct id
# then, IN the backup account, copy it to own it there (prod can't delete that copy)
# + S3 backups under Object Lock (WORM) so even the backup account can't erase them✅ Do
- Automate snapshot copies to a separate backup account in another region
- Give the backup copies their own KMS key and immutable (Object Lock) storage
- Scope access so production credentials can't delete the DR copies
❌ Don't
- Keep every backup in the production account/region
- Let one admin role hold delete rights over both the DB and all its backups
19Encryption & KMS in DR — the key must travel too▶
Scenario: disaster strikes, you copy your encrypted snapshot to the DR region, and the restore fails: the KMS key that encrypted it is region-bound and doesn't exist in the DR region. Your data is intact but unreadable where you need it. In DR, the encryption key is as critical as the backup — a backup you can't decrypt is no backup at all.
What
RDS/Aurora backups are encrypted with KMS keys that are region-specific. A cross-region copy must be re-encrypted with a key that exists in the destination region, and the DR role must have access to it.
Why
Confidentiality is required — but a key that only lives in the dead region makes the DR copy undecryptable. The key's availability is part of your recovery path.
How
Provision a DR-region KMS key up front, re-encrypt snapshots to it on copy, and grant the DR/restore roles decrypt permission — then test a restore end to end.
us-east-1: snapshot encrypted with KMS key A (region-bound)
│ copy to us-west-2 ── must RE-ENCRYPT with a us-west-2 key B
▼
us-west-2: snapshot encrypted with key B ✓ exists in DR region
restore role needs kms:Decrypt on key B ✓ granted
❌ forget the DR-region key → data is intact but UNREADABLE on recovery
test the full restore, decryption included, BEFORE the real disaster.# copy the snapshot AND re-encrypt with a key that EXISTS in the DR region
$ aws rds copy-db-snapshot \
--source-db-snapshot-identifier arn:aws:rds:us-east-1:...:snapshot:shop-2026-07-15 \
--target-db-snapshot-identifier shop-dr-2026-07-15 \
--kms-key-id arn:aws:kms:us-west-2:...:key/DR-KEY \ # destination-region key
--region us-west-2
# ensure the DR/restore role can decrypt with that key (policy/grant)
$ aws kms create-grant --key-id arn:aws:kms:us-west-2:...:key/DR-KEY \
--grantee-principal arn:aws:iam::...:role/dr-restore --operations Decrypt
# then TEST: restore the DR snapshot in us-west-2 end to end (topic 11)✅ Do
- Provision a KMS key in the DR region and re-encrypt snapshots to it on copy
- Grant the DR/restore roles decrypt on that key; store key access in the runbook
- Test a full cross-region restore including decryption
❌ Don't
- Copy an encrypted snapshot cross-region without a destination-region key
- Assume the restore role automatically has permission to the DR key
20Failover & promotion — manual vs automatic▶
Scenario: the primary is unresponsive. Do you wait for it to recover, or promote a standby now? Promote too eagerly during a brief network blip and you risk split-brain (two primaries taking writes); promote too late and you're extending the outage. Failover is a judgment call plus a mechanism — and knowing which is automatic and which is yours is half the battle.
What
Failover = switching the writer role to a standby. RDS Multi-AZ fails over automatically in-region; promoting a cross-region replica or a self-managed standby is usually a deliberate action.
Why
The mechanism is easy; the decision is hard. Premature promotion causes split-brain and data divergence; hesitation costs RTO. You need clear triggers and fencing.
How
Define failover criteria in advance, fence the old primary (ensure it can't keep taking writes), promote, then repoint apps (topic 21). Prefer one clear decision-maker.
primary unreachable?
│ is it DOWN, or just a network blip? ← the hard question
▼
FENCE old primary (stop it taking writes) ─ prevents split-brain
▼
PROMOTE standby → new primary (RDS Multi-AZ: automatic in-region;
▼ cross-region replica: manual/deliberate)
REPOINT apps to the new writer (topic 21)
❌ two primaries writing at once = split-brain = diverged data = pain# RDS Multi-AZ: automatic in-region failover (AWS handles it, ~60–120s) # you don't trigger it; you CAN force one to test: $ aws rds reboot-db-instance --db-instance-identifier shop-prod --force-failover # cross-region replica / self-managed: DELIBERATE promotion $ aws rds promote-read-replica --db-instance-identifier shop-dr-west # self-managed: pg_ctl promote / SELECT pg_promote(); # FENCE the old primary so it can't accept writes (avoid split-brain): # stop it, revoke its network/security-group ingress, or block via proxy $ aws rds stop-db-instance --db-instance-identifier shop-prod # if reachable
✅ Do
- Define clear failover triggers and one decision-maker in advance
- Fence the old primary (stop it / cut its access) before/at promotion
- Rehearse promotion so the mechanics aren't new during a real outage
❌ Don't
- Promote on a brief blip without fencing — split-brain diverges your data
- Assume a promoted replica can simply rejoin the old primary later (it can't)
21Connections through failover — endpoints, DNS & RDS Proxy▶
Scenario: you promoted the standby in 30 seconds — but the app is still hammering the dead primary's IP, because every service hard-coded the old host and DNS is cached for an hour. The database recovered; the application didn't. Getting connections to follow the failover is a first-class DR problem, not an afterthought.
What
How clients find the current writer after failover: RDS/Aurora endpoints (DNS names AWS repoints), low-TTL DNS / Route 53 failover records, and RDS Proxy (a connection layer that hides failover from the app).
Why
A perfect database failover is useless if apps keep dialing the old host. Endpoint strategy often determines your actual RTO more than the database switch itself.
How
Connect via the RDS/cluster endpoint (not a raw IP), keep DNS TTLs low, use Aurora's writer endpoint or RDS Proxy so promotion transparently redirects, and store connection details in a secret you can flip.
❌ app → 10.0.3.7 (old primary IP) → still dead after failover ✅ app → shop.cluster-xyz.rds.amazonaws.com (endpoint AWS repoints) ✅ app → RDS Proxy → proxy tracks the current writer, app never changes ✅ app → my-db.example.com (Route 53, low TTL, failover record) RTO includes: promote DB + connections finding the NEW writer cached DNS / hard-coded IPs / long TTLs quietly extend your outage.
# ✅ apps use the ENDPOINT, never a raw IP # DATABASE_URL=postgres://app@shop.cluster-xyz.us-east-1.rds.amazonaws.com/shop # RDS Proxy: apps connect to the proxy; it tracks the current writer + pools conns $ aws rds create-db-proxy --db-proxy-name shop-proxy --engine-family POSTGRESQL ... # → failover is transparent to the app; also smooths connection storms # Route 53 failover record with a LOW TTL for cross-region cutover $ aws route53 change-resource-record-sets ... # my-db.example.com, TTL 30 # and keep the connection string in a secret you can rotate to the new region $ aws secretsmanager update-secret --secret-id prod/db-url --secret-string ...
✅ Do
- Connect via RDS/cluster endpoints or RDS Proxy, never raw IPs
- Keep DNS TTLs low; store the connection string in a flip-able secret
- Include app repoint/DNS steps in the runbook and time them in drills
❌ Don't
- Hard-code the primary's IP/hostname in app config
- Forget connection-pool caching — pools may cling to the dead host until recycled
22Replication lag = your real RPO▶
Scenario: you promised an RPO of "a few seconds" backed by an async replica. Nobody watched the lag. During a traffic spike it silently grew to four minutes — so when the region failed, you lost four minutes of orders, not a few seconds. For asynchronous replication, the current lag is your current RPO, and unmonitored lag makes your RPO a lie.
What
Replication lag = how far behind a replica is from the primary. For async replicas, whatever the lag is at the moment of disaster is exactly the data you lose — your live RPO.
Why
Lag isn't constant — it grows under load, long transactions, or network strain. An RPO target is meaningless unless you continuously measure and alert on the lag that defines it.
How
Monitor lag from Postgres (pg_stat_replication) and CloudWatch (ReplicaLag), alert when it exceeds your RPO, and investigate spikes before a disaster turns them into loss.
primary ●────────────────────────────► now
replica ●──────────────────► (behind by "lag")
└── this gap = writes NOT yet on the replica
region fails NOW → you lose exactly "lag" worth of writes = your RPO
lag is dynamic: spikes under load / long txns / network → RPO drifts
alert if lag > RPO target · a 4-min lag silently broke your "few seconds"# from Postgres: how far behind is the standby, right now?
$ psql -c "SELECT client_addr, state,
write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
# on the replica itself, time-based lag:
$ psql -c "SELECT now() - pg_last_xact_replay_timestamp() AS replica_lag;"
# RDS/Aurora: CloudWatch ReplicaLag / AuroraGlobalDBReplicationLag
$ aws cloudwatch put-metric-alarm --alarm-name replica-lag-over-rpo \
--namespace AWS/RDS --metric-name ReplicaLag \
--threshold 30 --comparison-operator GreaterThanThreshold ... # alert > 30s✅ Do
- Continuously monitor replica lag (pg_stat_replication + CloudWatch)
- Alert when lag exceeds your RPO target and investigate spikes
- Track lag on cross-region replicas specifically — that's your regional RPO
❌ Don't
- Quote an RPO you don't measure — async lag drifts under load
- Ignore a slowly-growing lag until a disaster converts it into lost data
23Detecting a disaster — you can't recover what you don't notice▶
Scenario: data was silently corrupting for six hours before anyone noticed — so every backup in that window is poisoned, and your PITR target must go back further than you thought, losing far more. Recovery time starts at detection, not at failure. The faster you notice a disaster, the smaller it is.
What
The monitoring that turns a failure into an alert: instance health, replication lag (topic 22), backup/archive success, disk/WAL space, error spikes, and data corruption signals (checksum failures, anomaly checks).
Why
Undetected disasters grow — corruption spreads into backups, a stalled archiver widens your RPO gap. Fast detection bounds the blast radius and your recovery target.
How
Alarm on the DR-critical signals, enable Postgres data checksums, watch backup/WAL-archive success, and alert a human — detection is the first step of every recovery.
failure ●──────── silent for 6h ────────● noticed ●──── recover ────►
\___ corruption spread into backups ___/
(the longer undetected, the further back PITR must go = more loss)
alarm on the DR-critical signals:
instance down · replica lag > RPO · backup/WAL archive FAILED
disk / pg_wal filling · checksum failures · sudden error/latency spike
detection is step ZERO of recovery — you can't fix what you can't see.# enable page-level checksums so silent corruption is DETECTED, not spread
$ initdb --data-checksums ... # (or the RDS/Aurora managed equivalent)
$ psql -c "SHOW data_checksums;" # on
# alarm on the signals that mean "a disaster is starting"
$ aws cloudwatch put-metric-alarm --alarm-name wal-archive-failing \
--namespace ... --metric-name ...ArchiveFailures --threshold 1 ...
# also alert on: instance status, ReplicaLag > RPO, FreeStorageSpace low,
# backup job failures, and error-rate / latency anomalies from the app
$ aws rds describe-events --source-identifier shop-prod --source-type db-instance✅ Do
- Enable data checksums so silent corruption is caught, not replicated
- Alarm on backup/WAL-archive failures, lag, disk/WAL space, and instance health
- Treat a failed backup or a broken archiver as a page-worthy incident
❌ Don't
- Assume "no alert" means "all good" if you never alarmed on the DR signals
- Let corruption or a stalled archiver run unnoticed into your backup window
24The DR runbook — turning panic into procedure▶
Scenario: 3am, the database is down, and the one person who knew how to fail over is on a plane. Nobody else knows the promote command, where the DR key lives, or who can authorize the cutover. The backups were fine — the humans weren't ready. A written, tested runbook is what makes 3am a procedure instead of a panic.
What
A DR runbook: the step-by-step recovery procedure — who decides, the exact commands to promote/restore/repoint, where keys and backups live, RPO/RTO targets, and the comms/escalation plan.
Why
Under pressure, people improvise badly and forget steps. A runbook makes recovery repeatable by anyone on call, not just the one expert — and cuts RTO.
How
Write the concrete steps, assign roles, keep it current (drills catch staleness), store it where it's reachable when systems are down, and rehearse it.
DR RUNBOOK (stored OUTSIDE the systems it recovers) ├─ roles: incident lead · DB operator · comms · approver ├─ decide: failover criteria + who authorizes the cutover ├─ do: exact commands — promote / PITR restore / repoint (t15,20,21) ├─ keys: where the DR-region KMS key & backup account creds live (t19) ├─ verify: data checks before declaring recovered (t11) ├─ comms: status page, stakeholders, the legal/notify clock └─ targets: RPO/RTO for THIS system (t1) kept current by drills (t25) — a stale runbook fails at 3am.
# dr-runbook.md — excerpt (kept in a place reachable when prod is DOWN) $ cat dr-runbook.md ## roles lead=@oncall-lead db=@dba-oncall comms=@sev-comms ## decide promote if primary unreachable > 5m AND replica lag < RPO ## fence aws rds stop-db-instance --db-instance-identifier shop-prod ## recover aws rds promote-read-replica --db-instance-identifier shop-dr-west ## repoint update prod/db-url secret + Route53; recycle app pools (t21) ## keys DR KMS key: alias/dr-key (us-west-2); backup acct: 9999-... ## verify SELECT count(*) FROM orders; + smoke tests, THEN declare up ## notify status page + stakeholders + breach clock if data exposed
✅ Do
- Write concrete steps and exact commands; assign clear roles and an approver
- Store the runbook where it's reachable when your systems are down
- Keep it current — drills (topic 25) expose stale steps and missing access
❌ Don't
- Depend on one hero who "just knows how" — bus factor of one is a DR risk
- Keep the only copy of the runbook inside the system it's meant to recover
25DR game days — rehearsing the disaster on purpose▶
Scenario: your DR plan looks great on paper. Then you run a game day — deliberately fail over to the DR region — and discover the replica was under-provisioned, the runbook's promote command changed, the on-call engineer lacked the KMS permission, and the app's connection pool clung to the dead host for ten minutes. Every one of those is far cheaper to find on a Tuesday afternoon than during a real outage.
What
A game day (DR drill / chaos exercise): a scheduled, realistic rehearsal of a disaster — fail over regions, restore from backup, run the runbook end to end — and measure the result.
Why
Plans, backups, and runbooks are all unverified until exercised. Drills convert assumptions into measured RTO/RPO and surface the human, permission, and capacity gaps that only appear under a real cutover.
How
Schedule regular drills, run the actual failover/restore (ideally in a realistic env), time it against your targets, and file every gap as a fix. Increase realism over time.
GAME DAY: deliberately trigger the disaster & run the real runbook ┌────────────────────────────────────────────────────────────┐ │ 1 fail over to DR region / restore from backup │ │ 2 follow the runbook exactly (no shortcuts, on-call runs it) │ │ 3 TIME it → measured RTO/RPO vs targets (t1) │ │ 4 log every gap: under-sized replica, missing perm, stale cmd│ │ 5 fix them → re-drill │ └────────────────────────────────────────────────────────────┘ quarterly > annually. realism increases each time. gaps get cheaper.
# scheduled DR game day (quarterly) — run the ACTUAL runbook, timed $ START=$(date +%s) # 1) simulate the disaster (e.g. force a failover / restore in DR region) $ aws rds failover-global-cluster --global-cluster-identifier shop-global ... # 2) on-call follows dr-runbook.md verbatim: fence, promote, repoint, verify # 3) measure the real numbers $ echo "measured RTO: $(( $(date +%s) - START ))s (target: 300s)" # 4) capture gaps as tickets: replica too small? perm missing? DNS TTL high? # → fix, then schedule the next drill with more realism (real load, no heads-up)
✅ Do
- Schedule regular game days; run the real failover/restore, timed
- Have the on-call engineer (not the expert) run the runbook verbatim
- File every gap as a fix and increase realism each drill
❌ Don't
- Let the DR plan live only on paper, "verified" by never being run
- Always let the same expert drive — you're testing the plan, not the person
26Capstone — a region fails, recovered end to end★ capstone▶
Scenario: 02:11, us-east-1 is having a bad day and your primary is unreachable. This is the moment every topic in this playbook was for. Let's walk one regional disaster from alarm to "recovered," and watch each piece — RPO/RTO, replica, promotion, endpoints, keys, runbook, drills — do its job.
What
An end-to-end regional failover: detect, decide, fence, promote the cross-region copy, move connections, verify, communicate, and later recover the failed region — all driven by the runbook you rehearsed.
Why
DR isn't any single feature; it's the choreography of all of them under pressure. The capstone shows how the pieces connect into one calm procedure.
How
Follow the rehearsed runbook: detection → decision against RPO/RTO → fence old primary → promote → repoint → verify → comms → post-incident and region recovery.
02:11 DETECT alarm: primary unreachable, region impaired (t23, t22 lag) 02:14 DECIDE criteria met? RPO ok (lag was <10s)? approver yes (t1, t20) 02:16 FENCE ensure old primary can't take writes → no split-brain (t20) 02:17 PROMOTE promote cross-region replica / Global DB secondary (t14,17) 02:19 REPOINT flip endpoint/secret + Route53; recycle app pools (t21) 02:24 VERIFY row counts + smoke tests pass → declare recovered (t11) 02:25 COMMS status page + stakeholders + notify clock (t24) later RECOVER rebuild us-east-1, re-establish replication, post-mortem (t25) RTO ~14 min · RPO ~10s — because it was DESIGNED, rehearsed, and measured.
# 02:16 fence the old primary (prevent split-brain)
$ aws rds stop-db-instance --db-instance-identifier shop-prod # if reachable
# 02:17 promote the cross-region copy to the new writer
$ aws rds failover-global-cluster --global-cluster-identifier shop-global \
--target-db-cluster-identifier arn:aws:rds:us-west-2:...:cluster:shop-west
# 02:19 move connections: flip the secret + DNS, recycle pools
$ aws secretsmanager update-secret --secret-id prod/db-url --secret-string "...west..."
$ aws route53 change-resource-record-sets ... # my-db.example.com → west
# 02:24 verify BEFORE declaring recovered
$ psql -h my-db.example.com -c "SELECT count(*) FROM orders;" # sane → up
# later: rebuild us-east-1, re-establish replication, run the post-mortem (t25)✅ Do
- Drive the whole failover from the rehearsed runbook, roles assigned
- Verify data and app health before declaring recovered; then communicate
- Run a blameless post-mortem and turn findings into fixes and new drills
❌ Don't
- Improvise the sequence mid-incident, or skip fencing to "save time"
- Declare victory at "database promoted" — you're done when the app is serving users