PostgreSQLXVIII · Platforms, Corruption and Production ArchitecturePlatforms
Data corruption: signals and careful response
What you'll learn
- Recognise the signals that indicate corruption
- Respond without making the situation worse
- Understand why a passing check may be meaningless
- Choose between restoring and extracting
Prerequisites
Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27
Everything in this lesson was produced by writing eight bytes — the
string CORRUPTX — into byte offset 4000 of block 5 of a heap file,
with the cluster stopped.
The signals
ERROR: invalid page in block 5 of relation "base/5/16384"
LOG: page verification failed, calculated checksum 23084 but expected 18652
ERROR: could not read block N in file "...": read only 0 of 8192 bytes
ERROR: missing chunk number 0 for toast value ... in pg_toast_...
WARNING: concurrent insert in progress within table "..."
ERROR: index "..." contains unexpected zero page at block N
ERROR: found xmin ... from before relfrozenxid ...
Softer signals, which is where it usually starts:
- A query returns different results on the primary and a standby.
- A query returns different results with and without an index.
checksum_failuresnon-zero inpg_stat_database— lesson XVI-03 said alert on this at any value.- Rows that violate a constraint the database is supposedly enforcing.
- The operating system logging I/O errors.
What was measured
$ SELECT count(*) FROM victim;ERROR: invalid page in block 5 of relation "base/5/16384"
-- server log:
LOG: page verification failed, calculated checksum 23084 but expected 18652$ SELECT count(*) FROM victim WHERE id = 1; count
-------
1Most of the table still reads fine. The damage surfaces only when something touches the affected block, which is why corruption is frequently discovered by an unrelated query weeks after it happened.
Responding
The instinct is to fix it. Almost everything that feels like fixing it destroys evidence or makes recovery harder.
First: stop making it worse
- Do not restart unless you must. A restart may evict the corrupt page and turn a working system into a failing one — and it destroys the shared-memory state.
- Do not
VACUUM FULLorREINDEXthe affected object. Both rewrite, and a rewrite from corrupt input produces corrupt output with the original gone. - Do not
zero_damaged_pages. It discards data by design. - Do not fail over. If the corruption came from PostgreSQL or replicated, the standby has it too.
Second: capture
# take a physical copy before anything else
pg_ctl stop -m fast # only if you are certain, and only after deciding
cp -a $PGDATA /safe/place/pgdata-$(date +%s)
A copy taken now is the only thing that lets you try more than one recovery approach.
Third: establish the extent
-- which relation is base/5/16384?
SELECT c.relname, n.nspname FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relfilenode = 16384;
-- has this happened before?
SELECT datname, checksum_failures, checksum_last_failure
FROM pg_stat_database WHERE checksum_failures > 0;
And on a stopped cluster or a copy:
pg_checksums --check -D $PGDATA
$ pg_checksums --check -D /var/lib/postgresql/corpg_checksums: error: checksum verification failed in file
".../base/5/16384", block 5:
calculated checksum 5A2C but block contains 48DC
Files scanned: 955
Blocks scanned: 3212
Bad checksums: 1One block out of 3,212, named exactly. That is the number that decides what to do next.
Fourth: decide
| Situation | Action |
|---|---|
| A good backup exists, and the RPO is acceptable | Restore. The clean answer |
| No usable backup | Extract what you can, below |
| Corruption is spreading | Hardware. Move off it first |
| One index affected | REINDEX — indexes are derived, so this is safe |
| Heap affected | Restore. The heap is the data |
Extracting, when there is no backup
Last resort, on a copy, accepting that the result is not trustworthy:
-- 1. find which rows are unreadable, by ctid, block by block
-- and dump everything around them
-- 2. only if that fails:
SET zero_damaged_pages = on; -- DISCARDS the damaged pages entirely
zero_damaged_pages replaces unreadable pages with empty ones. The
table becomes readable and the rows in those pages are gone, with no
record of what they were. It is a way to rescue 99% of a table at the
cost of silently losing 1%, and every downstream consumer of that data
now has a hole nobody can characterise.
What to take from this
- Corruption is localised. Most of the object still reads, which is why it is found late.
- A check that passes on a warm cluster may have been reading shared
buffers. Measured:
amcheckpassed, then failed after a restart. - Verify against a restored copy, after a restart, or with
pg_checksumson a stopped cluster. - Do not restart, rewrite, reindex the heap, zero pages, or fail over before understanding it.
- Copy the data directory first. It is what lets you try twice.
pg_checksums --checknames the exact bad blocks. Measured: 1 of 3,212.ignore_checksum_failuremakes the database return answers from damaged pages. Measured.- Establish the source: storage, a lying
fsync, a bug, operator error, or a collation change — the last needs reindexing, not restoring.
Cross-course references
- Ceph & Distributed Storage — Part CXVIII (Data integrity incident) covers the same discipline in a storage system: take a copy first, and do not let a repair destroy the evidence.
- Linux for Production Sysadmins — Part LXXXI (Incident command) covers the authorisation this class of response needs, and Part LXXXII (Root cause analysis) covers establishing the source rather than stopping at the symptom.
- Observability for Production Sysadmins — Part LIX (Database
observability) covers alerting on
checksum_failures, which should never be non-zero.
Quiz
Knowledge check · 6 questions
Q1. An amcheck run with heapallindexed passes on a table, and the same check fails after a cluster restart. What explains this?
Q2. Corruption is discovered in a heap page. Which action makes recovery harder rather than easier?
Q3. After an operating system upgrade, a query returns different results depending on whether an index is used, with no errors anywhere. What is the likely cause and the correct response?
Q4. Which are signals that warrant investigating for corruption? Select all that apply.
Q5. Setting ignore_checksum_failure allows a cluster with a damaged page to continue serving trustworthy results.
Q6. Why should establishing the source of corruption precede deciding how to recover from it?
Passing score: 75%. Answers are checked in this browser.