Prove the capstone through safe failure drills: crash recovery, blocked locks, standby lag/loss, accidental change and PITR, backup corruption detection, configuration failure, load pressure, failover, and a final evidence-based design defense.

Run Failure, Corruption, Restore, Failover, and Performance Drills—and Defend the Design

Prove the capstone through safe failure drills: crash recovery, blocked locks, standby lag/loss, accidental change and PITR, backup corruption detection, configuration failure, load pressure, failover, and a final evidence-based design defense.

Intermediate → Advanced240–330 minutesProduction capstone · ServiceHubPostgreSQL 18.4 baseline verified 2026-08-18; re-check current minor before productionCore PostgreSQL mandatory path; external poolers/HA control planes are optionalDisposable local cluster ports: primary 55480 · standby 55481 · restore 55482Administrative labs require PostgreSQL server utilities and a disposable local data directoryNo paid service required; destructive drills target only ch24_* lab resourcesLast reviewed: August 2026

Learning outcomes

The capstone is not complete when the architecture diagram looks reasonable. It is complete when controlled failures produce expected detection, recovery, and correctness evidence—and when the team can state where the design still fails. Every drill below targets only the disposable Chapter 24 cluster, standby, restore copy, or backup copy. Never translate “failure injection” into corrupting an unmanaged production data directory.

01

Execute safe crash, lock, replica-lag, accidental-change, backup-corruption, configuration, and load-pressure drills.

02

Record detection time, recovery time, data correctness, RPO impact, and residual risk for each failure.

03

Use PostgreSQL logs/catalog/statistics and business invariants together instead of declaring success from process state alone.

04

Defend the design's explicit tradeoffs and identify unmet SLOs without hiding them.

05

Clean up the disposable topology only after evidence and rollback artifacts have been reviewed.

1. Use one drill record for every incident

sql · create an operations evidence table
SET ROLE servicehub_cap_owner;CREATE TABLE IF NOT EXISTS ops.ch24_drill_log (  drill_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,  drill_name text NOT NULL,  started_at timestamptz NOT NULL,  detected_at timestamptz,  recovered_at timestamptz,  expected_result text NOT NULL,  observed_result text,  data_correct boolean,  rpo_seconds numeric,  rto_seconds numeric,  notes text);RESET ROLE;

Do not fabricate values. Start each drill, capture wall-clock timestamps from the lab, and write observed facts afterward. A failed drill is useful engineering evidence when the gap is documented and repaired.

2. Drill A — immediate process loss and crash recovery

pg_ctl stop -m immediate simulates abrupt server-process loss without clean shutdown. WAL crash recovery should return the cluster to a consistent transactional state; it does not recover missing/corrupted storage.

shell · Unix immediate-stop drill
date -upg_ctl -D "$CH24_PRIMARY" stop -m immediate# Observe the process is down, then restart.pg_ctl -D "$CH24_PRIMARY" \  -l "$CH24_ROOT/primary.log" \  -o "-p 55480 -c listen_addresses=127.0.0.1" starttail -n 80 "$CH24_ROOT/primary.log"
powershell · Windows immediate-stop drill
Get-Date -AsUTCpg_ctl.exe -D $env:CH24_PRIMARY stop -m immediatepg_ctl.exe -D $env:CH24_PRIMARY `  -l (Join-Path $env:CH24_ROOT "primary.log") `  -o "-p 55480 -c listen_addresses=127.0.0.1" startGet-Content (Join-Path $env:CH24_ROOT "primary.log") -Tail 80
sql · post-crash business checks
SELECT pg_is_in_recovery();SELECT count(*) AS orders,       count(*) FILTER (WHERE status NOT IN         ('queued','assigned','in_progress','completed','cancelled'))         AS invalid_statusFROM app.work_orders;SELECT conname,convalidatedFROM pg_constraintWHERE conrelid='app.work_orders'::regclassORDER BY conname;

Pass requires server recovery plus valid business state—not just “port 55480 accepts TCP.” Record crash-recovery log evidence and actual RTO.

3. Drill B — blocked transaction and lock queue

sql · Session A — hold a row lock
BEGIN;UPDATE app.work_ordersSET note = note || '-lock-drill'WHERE tenant_id = (  SELECT tenant_id FROM app.tenants WHERE tenant_code='tenant-a')AND work_order_id = (  SELECT min(work_order_id)  FROM app.work_orders  WHERE tenant_id = (    SELECT tenant_id FROM app.tenants WHERE tenant_code='tenant-a'  ));-- Keep this transaction open until diagnosis is complete.
sql · Session B — bounded waiter
SET lock_timeout='3s';UPDATE app.work_ordersSET amount = amount + 0.01WHERE tenant_id = (  SELECT tenant_id FROM app.tenants WHERE tenant_code='tenant-a')AND work_order_id = (  SELECT min(work_order_id)  FROM app.work_orders  WHERE tenant_id = (    SELECT tenant_id FROM app.tenants WHERE tenant_code='tenant-a'  ));
sql · Session C — identify blocker/waiter
SELECT a.pid,a.state,a.wait_event_type,a.wait_event,       now()-a.xact_start AS xact_age,       pg_blocking_pids(a.pid) AS blocking_pids,       a.query_idFROM pg_stat_activity AS aWHERE a.datname=current_database()ORDER BY a.xact_start NULLS LAST;

Repair by ending Session A with ROLLBACK in this controlled lab. In production, do not terminate a blocker until you identify ownership, transaction purpose, rollback cost, and application retry semantics.

4. Drill C — standby replay pause, lag, and WAL retention

On the standby, an administrator can pause WAL replay. The receiver may continue receiving/flushing while replay stops; this demonstrates why “connected” does not mean “caught up.”

sql · standby port 55481 — pause
SELECT pg_wal_replay_pause();SELECT pg_get_wal_replay_pause_state(),       pg_last_wal_receive_lsn(),       pg_last_wal_replay_lsn();
sql · primary — generate controlled WAL and observe
INSERT INTO app.work_orders(tenant_id,customer_id,external_ref,status,scheduled_at,amount,note,priority)SELECT c.tenant_id,c.customer_id,       'LAG-' || g,       'queued',       clock_timestamp() + g * INTERVAL '1 second',       25.00,'lag-drill',3FROM app.customers AS cCROSS JOIN generate_series(1,1000) AS gWHERE c.tenant_id=(SELECT tenant_id FROM app.tenants WHERE tenant_code='tenant-a')  AND c.customer_id=(SELECT min(customer_id) FROM app.customers                     WHERE tenant_id=c.tenant_id)ON CONFLICT (tenant_id,external_ref) DO NOTHING;SELECT application_name,state,       sent_lsn,write_lsn,flush_lsn,replay_lsn,       pg_size_pretty(pg_wal_lsn_diff(sent_lsn,replay_lsn)) AS replay_byte_lagFROM pg_stat_replication;SELECT slot_name,active,restart_lsn,wal_status,safe_wal_sizeFROM pg_replication_slotsWHERE slot_name='ch24_standby_slot';
sql · standby — resume and verify convergence
SELECT pg_wal_replay_resume();SELECT pg_get_wal_replay_pause_state(),       pg_last_wal_receive_lsn(),       pg_last_wal_replay_lsn();

The drill passes only after replay catches up and primary retained-WAL capacity remains healthy. Never “fix” slot retention by deleting files from pg_wal.

5. Drill D — accidental data change and PITR decision

sql · create a fresh restore boundary then make a bad change
SELECT pg_create_restore_point('ch24_drill_before_delete');SELECT count(*) AS before_rowsFROM app.work_ordersWHERE status='completed';DELETE FROM app.work_ordersWHERE status='completed'  AND created_at < TIMESTAMPTZ '2026-09-01 00:00+00';SELECT count(*) AS after_rowsFROM app.work_ordersWHERE status='completed';

Do not “undo” a broad DELETE by guessing an inverse INSERT. Choose between application-level repair and PITR based on scope, dependencies, time, and the RPO/RTO contract. For PITR, repeat Lesson 4's base-backup/WAL-archive restore into CH24_RESTORE with recovery_target_name='ch24_drill_before_delete', verify business state, then decide cutover. The original primary remains untouched until the recovery decision is explicit.

6. Drill E — corrupt a COPY of the backup and prove detection

Physical corruption injection belongs in a copied backup artifact, not a running cluster. Use the backup manifest to let pg_verifybackup detect that a file no longer matches its recorded checksum.

shell · Unix copy and corrupt one backed-up relation byte
CORRUPT="$CH24_ROOT/corrupt_backup"rm -rf "$CORRUPT"cp -a "$CH24_BACKUP" "$CORRUPT"REL_PATH="$(psql -h 127.0.0.1 -p 55480 -U postgres \  -d servicehub_capstone -Atc \  "SELECT pg_relation_filepath('app.work_orders');")"python3 - "$CORRUPT/$REL_PATH" <<'PY'from pathlib import Pathimport sysp=Path(sys.argv[1])with p.open("r+b") as f:    f.seek(min(9000, max(0, p.stat().st_size-1)))    b=f.read(1)    f.seek(-1,1)    f.write(bytes([(b[0] ^ 0x01) if b else 0x01]))PYpg_verifybackup "$CORRUPT" || true
powershell · Windows backup-corruption equivalent
$corrupt = Join-Path $env:CH24_ROOT "corrupt_backup"if (Test-Path $corrupt) { Remove-Item -Recurse -Force $corrupt }Copy-Item -Recurse $env:CH24_BACKUP $corrupt$rel = psql.exe -h 127.0.0.1 -p 55480 -U postgres `  -d servicehub_capstone -Atc `  "SELECT pg_relation_filepath('app.work_orders');"$target = Join-Path $corrupt $rel.Trim()$bytes = [IO.File]::ReadAllBytes($target)$offset = [Math]::Min(9000, $bytes.Length - 1)$bytes[$offset] = $bytes[$offset] -bxor 1[IO.File]::WriteAllBytes($target,$bytes)pg_verifybackup.exe $corrupt

Expected: verification fails and names a checksum/file mismatch. This proves the manifest detects this corruption class; it does not prove every possible storage/CPU/memory corruption path or validate the application.

7. Drill F — configuration error must fail visibly and be reversible

Use the standby or restore copy—not the primary—to inject a syntactically invalid memory setting.

conf · disposable standby misconfiguration
# Add temporarily to the standby's postgresql.conf:shared_buffers = 'not-a-size'
shell · observe startup/config failure and repair
pg_ctl -D "$CH24_STANDBY" stop -m fast# After adding the invalid line:pg_ctl -D "$CH24_STANDBY" \  -l "$CH24_ROOT/standby-config-error.log" \  -o "-p 55481" start || truetail -n 60 "$CH24_ROOT/standby-config-error.log"# Remove/comment the invalid line, then:pg_ctl -D "$CH24_STANDBY" \  -l "$CH24_ROOT/standby.log" \  -o "-p 55481" start

The pass condition is that monitoring notices the node is unavailable, the log identifies the setting problem, the operator can repair it without touching primary data, and replication resumes.

8. Drill G — bounded load spike and connection pressure

A load drill should declare concurrency, query, duration, cache state, JIT/parallel settings, and host resources. The fan-out commands below assume libpq can authenticate non-interactively through a protected passfile (PGPASSFILE / pgpass.conf) or another approved local secret mechanism. Do not place a password in the command line or source file. The example launches only eight clients against the disposable cluster.

shell · Unix bounded client fan-out
for i in $(seq 1 8); do  psql -h 127.0.0.1 -p 55480 -U postgres \    -d servicehub_capstone -c "      SELECT tenant_id,status,count(*),sum(amount)      FROM app.work_orders      GROUP BY tenant_id,status      ORDER BY tenant_id,status;" \    >/dev/null &donewait
powershell · Windows bounded client fan-out
$jobs = 1..8 | ForEach-Object {  Start-Job -ScriptBlock {    psql.exe -h 127.0.0.1 -p 55480 -U postgres `      -d servicehub_capstone -c `      "SELECT tenant_id,status,count(*),sum(amount)       FROM app.work_orders       GROUP BY tenant_id,status       ORDER BY tenant_id,status;" | Out-Null  }}$jobs | Wait-Job | Receive-Job | Out-Null$jobs | Remove-Job
sql · observe while the spike runs
SELECT state,count(*) AS sessionsFROM pg_stat_activityWHERE backend_type='client backend'GROUP BY state;SELECT wait_event_type,wait_event,count(*) AS backendsFROM pg_stat_activityWHERE state='active'GROUP BY wait_event_type,wait_eventORDER BY backends DESC;SELECT numbackends,deadlocks,temp_bytes,blk_read_time,blk_write_timeFROM pg_stat_databaseWHERE datname=current_database();

Eight clients are not a performance claim. The goal is to prove the evidence path and determine whether the system saturates CPU, I/O, memory, locks, or connection admission before changing configuration.

9. Drill H — operator failover with explicit fencing

text · failover stopwatch checklist
START RTO CLOCK[ ] Stop application writes / routing to primary[ ] Fence old primary so it cannot accept writes[ ] Record standby replay position and expected RPO impact[ ] Promote standby[ ] Verify pg_is_in_recovery() = false[ ] Run one business read/write invariant[ ] Reroute clients[ ] Verify new writes only on promoted node[ ] Keep old primary isolated[ ] Record timeline / rebuild planSTOP RTO CLOCKPass:- data correctness accepted- measured RPO <= 60 s target- measured RTO <= 15 min targetIf not met:- record exact step/time that breached the SLO; do not relabel the target.

10. Defend the design with evidence and admitted gaps

Decision Evidence to defend it Residual risk / next improvement
async physical standby measured replay lag + failover drill stays inside 60 s RPO single-host lab proves mechanism, not independent failure domain; production needs separate host/AZ/site decision
WAL archive + PITR manifest verification + named-target restore + business invariants lab archive is local; production requires off-host/immutable retention and access controls
RLS tenant isolation cross-tenant read/write denial under authenticated role mapping role-per-tenant lab may not scale; production identity propagation needs equivalent non-forgeable trust boundary
monthly event partitions pruning evidence + retention runbook future partition automation/failure alert required
targeted index/autovacuum/memory changes before/after EXPLAIN/stats evidence synthetic data/load is not production capacity certification

11. Final acceptance query and design-defense rubric

sql · database-side final evidence snapshot
SELECT current_database(),version();SELECT count(*) AS invalid_constraintsFROM pg_constraintWHERE connamespace='app'::regnamespace  AND NOT convalidated;SELECT count(*) AS invalid_indexesFROM pg_indexWHERE NOT indisvalid  AND indrelid IN (    SELECT oid FROM pg_class    WHERE relnamespace='app'::regnamespace  );SELECT relname,n_live_tup,n_dead_tup,last_autovacuum,last_autoanalyzeFROM pg_stat_user_tablesWHERE schemaname='app'ORDER BY relname;SELECT archived_count,failed_count,last_archived_time,last_failed_timeFROM pg_stat_archiver;SELECT slot_name,active,wal_status,safe_wal_sizeFROM pg_replication_slotsORDER BY slot_name;

Zero invalid constraints/indexes is necessary but not sufficient. The defense also needs SLO load-test results, restore/failover timing, RLS tests, capacity forecast versus observed growth, current release/security posture, external routing/fencing evidence, and an owner for every open risk.

12. Deliberately wrong conclusion: “all drills passed, therefore production-ready”

Incomplete conclusion

A single laptop lab cannot prove independent failure domains, network partitions, cloud/SAN behavior, production client-driver compatibility, certificate lifecycle, real workload distribution, operator on-call response, or business disaster-recovery governance. Passing the capstone proves mechanism understanding and a testable design—not universal production certification.

13. Cleanup only after evidence review

Keep the lab until you have saved the drill log, plans, log excerpts, backup verification output, actual RPO/RTO timings, and ADR updates. If the failover drill left the original primary fenced, do not reconnect it to the application merely to clean up a slot. Either administer it in isolation or simply remove the dedicated Chapter 24 data directory after all evidence is saved.

sql · database cleanup before stopping the lab
-- Export ops.ch24_drill_log before destruction.SELECT * FROM ops.ch24_drill_log ORDER BY drill_id;-- Run the slot cleanup only on the node that owns the slot,-- only while it is isolated from application write routing.SELECT pg_drop_replication_slot('ch24_standby_slot')WHERE EXISTS (  SELECT 1  FROM pg_replication_slots  WHERE slot_name='ch24_standby_slot'    AND NOT active);
shell · Unix final lab cleanup
pg_ctl -D "$CH24_RESTORE" stop -m fast 2>/dev/null || truepg_ctl -D "$CH24_STANDBY" stop -m fast 2>/dev/null || truepg_ctl -D "$CH24_PRIMARY" stop -m fast 2>/dev/null || truerm -rf "$CH24_ROOT"
powershell · Windows final lab cleanup
pg_ctl.exe -D $env:CH24_RESTORE stop -m fast 2>$nullpg_ctl.exe -D $env:CH24_STANDBY stop -m fast 2>$nullpg_ctl.exe -D $env:CH24_PRIMARY stop -m fast 2>$nullRemove-Item -Recurse -Force $env:CH24_ROOT
Final production judgment

A defensible PostgreSQL platform states what it promises, shows evidence for those promises, separates mechanisms that solve different failures, and records what remains unproven. Keep the runbooks, restore drills, upgrade checks, RLS tests, capacity forecasts, and workload baselines alive after launch; production engineering is an operating loop, not a one-time configuration event.

Check your understanding

  1. Why is an immediate-stop drill not a corruption-recovery test?
  2. What does pausing standby replay demonstrate about replication health?
  3. Why is corrupting a copied base backup safer and more useful than touching a live relation file?
  4. What is the pass condition for a failover drill besides successful promotion?
  5. Why does passing this single-host capstone not certify a real production deployment?
Review the answers

Crash recovery replays WAL to transaction consistency but assumes storage is readable; corruption is a different failure class. Replay pause separates receive/flush from apply and exposes lag/retention behavior. A copied backup is disposable and pg_verifybackup can compare it to the manifest without risking live data. Failover must include fencing, routing, business correctness, and measured RPO/RTO—not just promotion. A laptop lab cannot reproduce independent failure domains, real traffic, platform networking/storage, certificate/secret lifecycle, or human on-call behavior.

Authoritative references

Use current upstream PostgreSQL documentation and release/support pages as the source of truth for version-, security-, topology-, and recovery-sensitive behavior.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.