PostgreSQL Crash Recovery and Data Checksums
`kill -9` the whole cluster with a transaction open, then read what recovery did. Five committed rows survived, the sixth was never there.
Troubleshooting Guide 47 of 47 Advanced
- OSUbuntu 26.04 LTS (resolute)
- PostgreSQL18.6-0ubuntu0.26.04.1
- PackagingDebian/Ubuntu (pg_ctlcluster, /etc/postgresql)
- TimeAbout 18 min
- Reviewed27 August 2026
Written against the versions above. `pg_checksums` can also enable checksums on an existing cluster offline, from PostgreSQL 12. Before that it was initdb-time only.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| db-b01 | 192.168.0.82 | Ubuntu 26.04 LTS | Standby / Replica / Replica Set Member 2 | 2 Core | 4 GB | 50 GB |
Before you start
- A cluster you can kill - not a production server.
sudo, and the cluster stopped briefly for the checksum scan.
-
What the control file says while the server is up
data_checksumsison- every data page carries a checksum verified on read, which is how torn or silently corrupted pages get caught rather than served.pg_controldatareads the 8 kB control file directly, no server needed:Database cluster state: in production- the flag recovery keys off. Set on start, cleared on clean shutdown.Latest checkpoint's REDO location: 0/17000028- where recovery would begin. Everything written after this point exists only in WAL until the next checkpoint.TimeLineID: 2- this host is a promoted standby, so it is on the second timeline.bash Example session sudo -u postgres psql -c "SHOW data_checksums" data_checksums---------------- on(1 row)sudo -u postgres /usr/lib/postgresql/18/bin/pg_controldata /var/lib/postgresql/18/main | head -12pg_control version number: 1800Catalog version number: 202506291Database system identifier: 7678708104239117136Database cluster state: in productionpg_control last modified: Thu 27 Aug 2026 04:37:17 PM UTCLatest checkpoint location: 0/17000028Latest checkpoint's REDO location: 0/17000028Latest checkpoint's REDO WAL file: 000000020000000000000017Latest checkpoint's TimeLineID: 2Latest checkpoint's PrevTimeLineID: 2Latest checkpoint's full_page_writes: onLatest checkpoint's NextXID: 0:849Expected resultChecksums on, state
in production, timeline 2.Success conditionYou can read a cluster's recovery state without connecting to it.
-
Write something committed and something not
Five rows inserted and committed. Then a second session runs a file that opens a transaction, inserts a sixth row and sleeps inside it.
The other session sees 5. The sixth row exists in that backend's snapshot and in WAL, and in nobody else's view of the world - which is exactly what an uncommitted row is.
Running the SQL from a file rather than
-cavoids nesting quotes throughsudo,bash -candpsql. That is a practical point rather than a PostgreSQL one, and it prevents a whole family of "column does not exist" errors caused by shell quoting turning a string literal into an identifier.bash Example session sudo -u postgres psql -d appdb -c "CREATE TABLE crashtest (id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, note text)"CREATE TABLEsudo -u postgres psql -d appdb -c "INSERT INTO crashtest (note) SELECT 'committed row ' || g FROM generate_series(1,5) g"INSERT 0 5sudo -u postgres psql -d appdb -c "SELECT count(*) AS committed_rows FROM crashtest" committed_rows---------------- 5(1 row)sudo -u postgres tee /tmp/uncommitted.sql > /dev/null <<'SQL'BEGIN;INSERT INTO crashtest (note) VALUES ('uncommitted row');SELECT pg_sleep(300);SQLsudo cat /tmp/uncommitted.sqlBEGIN;INSERT INTO crashtest (note) VALUES ('uncommitted row');SELECT pg_sleep(300);sudo bash -c 'nohup sudo -u postgres psql -d appdb -f /tmp/uncommitted.sql >/tmp/uncommitted.log 2>&1 & sleep 3; echo holder started'holder startedsudo -u postgres psql -d appdb -c "SELECT count(*) AS visible_to_others FROM crashtest" visible_to_others------------------- 5(1 row)Expected resultFive rows visible, a sixth in flight.
Success conditionYou have committed and uncommitted work in the same table.
-
Kill it
pkill -9 -x postgreskills the postmaster and every backend withSIGKILL. There is no shutdown, no final checkpoint, no chance to flush. It is the closest thing to pulling the power cable.systemd notices:
is-activereturnsfailed.Starting it produces the four lines that describe a crash recovery:
database system was interrupted; last known up at 16:37:17database system was not properly shut down; automatic recovery in progress- the control file saidin production, so the shutdown was not clean.redo starts at 0/170000A0- the last checkpoint's REDO location, exactly aspg_controldatareported it.redo done at 0/17028FD8- 166 kB of WAL replayed, in under a millisecond.Then
ready to accept connections. Nothing was asked of the operator. Recovery is automatic and not optional; there is no flag to skip it. Recovery time is bounded by how much WAL was written since the last checkpoint, which is whatmax_wal_sizeandcheckpoint_timeoutreally control.bash Example session sudo pkill -9 -x postgressystemctl is-active postgresql@18-mainfailed[exit 3]sudo systemctl start postgresql@18-mainsudo grep -E "was interrupted|not properly shut down|redo starts at|redo done|ready to accept" /var/log/postgresql/postgresql-18-main.log | tail -52026-08-27 16:38:09.739 UTC [23474] LOG: database system was interrupted; last known up at 2026-08-27 16:37:17 UTC2026-08-27 16:38:10.614 UTC [23474] LOG: database system was not properly shut down; automatic recovery in progress2026-08-27 16:38:10.617 UTC [23474] LOG: redo starts at 0/170000A02026-08-27 16:38:10.618 UTC [23474] LOG: redo done at 0/17028FD8 system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s2026-08-27 16:38:10.671 UTC [23468] LOG: database system is ready to accept connectionsExpected result
automatic recovery in progress, redo start and end, then ready.Success conditionYou can read a crash recovery in the log and know it completed.
-
Check what survived
Five rows. The five committed ones, ids 1 to 5, exactly as written.
The sixth is gone, and its session's log shows why:
BEGIN,INSERT 0 1, thenserver closed the connection unexpectedly. The insert *succeeded*. It was in WAL. Recovery replayed it and then rolled it back, because the transaction never committed.That is durability stated precisely: committed means it survives, and nothing else does.
INSERT 0 1is not a promise;COMMITis.The identity column is worth noticing too - the sixth row consumed an id, and that id is not reused. Sequences are deliberately not transactional, which is why identity columns have gaps after any rollback, crash or otherwise.
pg_control_checkpoint()shows the checkpoint written after recovery finished.bash Example session sudo -u postgres psql -d appdb -c "SELECT count(*) AS rows_after_crash FROM crashtest" rows_after_crash------------------ 5(1 row)sudo -u postgres psql -d appdb -c "SELECT id, note FROM crashtest ORDER BY id" id | note----+----------------- 1 | committed row 1 2 | committed row 2 3 | committed row 3 4 | committed row 4 5 | committed row 5(5 rows)cat /tmp/uncommitted.logBEGININSERT 0 1psql:/tmp/uncommitted.sql:3: server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request.psql:/tmp/uncommitted.sql:3: error: connection to server was lostsudo -u postgres psql -c "SELECT checkpoint_lsn, redo_lsn FROM pg_control_checkpoint()" checkpoint_lsn | redo_lsn----------------+------------ 0/17029000 | 0/17029000(1 row)Expected resultFive rows;
INSERT 0 1in the log of the row that no longer exists.Success conditionYou can state exactly what a crash does and does not lose.
-
Verify every page on disk
With the cluster stopped,
pg_controldatareportsDatabase cluster state: shut down- the clean-shutdown flag. Compare that within productionearlier; that one field is the whole basis for deciding whether to run recovery.pg_checksums --checkthen reads every page:Files scanned: 1576 / Blocks scanned: 4993 / Bad checksums: 0A hard kill mid-transaction damaged nothing, which is what full page writes and WAL exist to guarantee.
It requires the cluster to be stopped - it reads files directly and a running server would be writing underneath it. That makes it a maintenance-window tool, and the one worth reaching for after a storage incident, a restore from an unfamiliar backup, or any "the disk did something odd" report.
Starting the cluster sets the state back to
in production, and the next clean shutdown will clear it again.bash Example session sudo systemctl stop postgresql@18-mainsudo -u postgres /usr/lib/postgresql/18/bin/pg_controldata /var/lib/postgresql/18/main | grep -E "cluster state|checksum"Database cluster state: shut downData page checksum version: 1sudo -u postgres /usr/lib/postgresql/18/bin/pg_checksums --check -D /var/lib/postgresql/18/mainChecksum operation completedFiles scanned: 1576Blocks scanned: 4993Bad checksums: 0Data checksum version: 1sudo -u postgres /usr/lib/postgresql/18/bin/pg_controldata /var/lib/postgresql/18/main | grep -E "cluster state"Database cluster state: in productionsudo -u postgres psql -d appdb -c "DROP TABLE crashtest"DROP TABLEExpected result
shut down, 4,993 blocks scanned, zero bad checksums.Success conditionYou can prove a cluster's pages are intact after an incident.
Troubleshooting
Recovery is taking a long time after a crash.
Why: A lot of WAL was written since the last checkpoint.
Fix:Nothing to do but wait. Reduce
max_wal_sizeorcheckpoint_timeoutfor next time.Rows that a client reported as inserted are missing after a crash.
Why: The transaction had not committed.
Fix:Nothing was lost that was promised. Check the client's commit handling.
invalid page in block ... of relation ...in the log.Why: A checksum failed - the page on disk is not what was written.
Fix:Restore from backup.
zero_damaged_pagesdiscards data and is a last resort.pg_checksumsrefuses to run.Why: The cluster is still running, or checksums were never enabled.
Fix:Stop it first; check
SHOW data_checksums.Identity or serial values have gaps after a crash.
Why: Sequences are not transactional, by design.
Fix:Expected. Do not rely on identity columns being contiguous.
Official sources
- PostgreSQL 18 Documentation - Client Authentication
- PostgreSQL 18 Documentation - Server Start-up Failures
- PostgreSQL 18 Documentation - Continuous Archiving
- PostgreSQL 18 Documentation - Preventing Transaction ID Wraparound Failures
- PostgreSQL 18 Documentation - Reliability and the Write-Ahead Log
- PostgreSQL 18 Documentation - pg_checksums