Skip to main content
RunBook Academy

PostgreSQLXVIII · Platforms, Corruption and Production ArchitecturePlatforms

Data corruption: signals and careful response

Advanced⏱ ~35 min🧪 Lab requiredpsql

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

Not yet marked complete on this device.

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_failures non-zero in pg_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

Data-loss riskthe query, after eight bytes were changed
$ 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
Read-only / Safecorruption is localised, and therefore partly invisible
$ SELECT count(*) FROM victim WHERE id = 1;
 count
-------
   1

Most 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

  1. 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.
  2. Do not VACUUM FULL or REINDEX the affected object. Both rewrite, and a rewrite from corrupt input produces corrupt output with the original gone.
  3. Do not zero_damaged_pages. It discards data by design.
  4. 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
Read-only / Safepg_checksums finding exactly one bad block
$ pg_checksums --check -D /var/lib/postgresql/cor
pg_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:  1

One block out of 3,212, named exactly. That is the number that decides what to do next.

Fourth: decide

SituationAction
A good backup exists, and the RPO is acceptableRestore. The clean answer
No usable backupExtract what you can, below
Corruption is spreadingHardware. Move off it first
One index affectedREINDEX — indexes are derived, so this is safe
Heap affectedRestore. 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: amcheck passed, then failed after a restart.
  • Verify against a restored copy, after a restart, or with pg_checksums on 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 --check names the exact bad blocks. Measured: 1 of 3,212.
  • ignore_checksum_failure makes 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

  1. Q1. An amcheck run with heapallindexed passes on a table, and the same check fails after a cluster restart. What explains this?

  2. Q2. Corruption is discovered in a heap page. Which action makes recovery harder rather than easier?

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

  4. Q4. Which are signals that warrant investigating for corruption? Select all that apply.

  5. Q5. Setting ignore_checksum_failure allows a cluster with a damaged page to continue serving trustworthy results.

  6. Q6. Why should establishing the source of corruption precede deciding how to recover from it?

Passing score: 75%. Answers are checked in this browser.