pgBackRest 2.60: Page Checksum Errors and How to Survive Them
pgBackRest 2.60.0 shipped on 18 October 2026, and the headline change is not a performance win or a new repository type. It is a behavior change that will break scripted restores you have trusted for years: the restore command now errors and stops when a restored file contains an invalid page checksum.
That is the right default. A restore that silently produces a cluster full of corrupt pages is worse than one that fails loudly, because you only discover the damage after you have opened writes on it. But it does mean you should rehearse the new behavior on purpose, in a maintenance window, before you need it at 3 a.m. Here is the walkthrough.
Step 1: Understand exactly what changed in 2.60.0
One option changed, so it is easy to underread the release notes. The new checksum-page-error option applies to three commands, and each one defaults differently:
- backup — warns per file, exactly as before, and now also reports the total number of checksum warnings at the end. No hard failure.
- restore — errors by default. Previously restore did not report invalid page checksums at all.
- expire — errors by default. It refuses to expire when the oldest retained backup would contain checksum errors, which also blocks archive expiration in that repository.
The restore failure mode is the one to internalize: the restore stops partway through, the cluster is incomplete and will not start, and the restore must be rerun. Your runbook needs to be idempotent, not merely correct.
Step 2: Confirm your cluster actually has data checksums
pgBackRest can only validate pages that PostgreSQL checksummed in the first place. checksum-page is enabled automatically when the cluster has data checksums turned on — which means everything below is a no-op on a cluster where checksums are off.
# Works on any running cluster
psql -Atc "show data_checksums;" # expected: on
# Or from the data directory, with the server stopped
pg_controldata "$PGDATA" | grep -i checksum
If that returns off, you have a bigger finding than this upgrade. Turning checksums on for an existing cluster is offline work: stop the server, run pg_checksums --enable -D "$PGDATA", then start it. Budget a maintenance window and a full backup afterwards — and treat the first checksummed backup as a corruption audit, because it will surface pages that have been quietly broken for years.
Step 3: Upgrade, then run a deliberate full backup
Install from the PostgreSQL repositories so package versions track the server you run. Note that PostgreSQL 19rc1 requires pgBackRest 2.59.2 or later, so 2.60.0 covers it.
# Debian/Ubuntu
sudo apt-get update && sudo apt-get install -y pgbackrest
# RHEL/Rocky
sudo dnf install -y pgbackrest
pgbackrest version # expect: pgBackRest 2.60.0
# Full backup, keeping warning detail on the console
sudo -u postgres pgbackrest --stanza=main \
--log-level-console=detail --type=full backup
Read the tail of that output carefully. The new summary counts checksum warnings across the whole backup, so a cluster you believed was healthy can suddenly report “N checksum warnings” in one line. That count is the number of files with at least one bad page — a strong signal to investigate before the next restore.
Step 4: Map the warnings back to real tables
Warnings reference files under $PGDATA, not table names. For anything in the default tablespace, map the segment number back to a relation with pg_filenode_relation — or, more simply, match relfilenode directly:
-- Given a warning for $PGDATA/base/16384/24601
SELECT n.nspname, c.relname, c.relkind, pg_size_pretty(pg_total_relation_size(c.oid)) AS size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relfilenode = 24601;
-- Confirm which database owns that directory
SELECT oid, datname FROM pg_database WHERE oid = 16384;
An invalid page in a frequently written heap is usually hardware, a flaky page cache, or a torn write from an unplanned crash. An invalid page in a cold archive table is often much older than the incident you are chasing. Either way the remediation is the same: restore the affected relation from a known-good backup or rebuild it, and check the storage underneath before you trust the next backup.
Step 5: Choose your policy in pgbackrest.conf
Leave the defaults alone unless you have a written reason not to. Setting checksum-page=y explicitly documents the dependency.
[global]
repo1-path=/var/lib/pgbackrest
repo1-retention-full=4
checksum-page=y
# 2.60.0 defaults: backup warns, restore and expire error.
# checksum-page-error=n restores warn-only behavior — but it also lets a
# restore finish on corrupt data. Avoid it in the config file.
[main]
pg1-path=/var/lib/postgresql/18/main
The single case for warn-only is a partially corrupt cluster where getting most of the data back beats getting none — a disaster-recovery judgment call, not a default. Make it an ad-hoc flag on one command (pgbackrest --checksum-page-error=n restore ...), not a line in the config file where it would silently govern every future restore.
Step 6: Make the failed-restore path boring
Because a failed restore leaves an incomplete cluster, the recovery steps must be safe to repeat. Verify repository integrity without touching the cluster first, then restore onto a clean path rather than over a half-restored one.
# 1. Verify the repository without restoring anything
sudo -u postgres pgbackrest --stanza=main verify
# 2. Stop the cluster and move the old data directory aside
sudo pg_ctlcluster 18 main stop
sudo mv /var/lib/postgresql/18/main /var/lib/postgresql/18/main.failed.$(date +%s)
# 3. Restore and start
sudo -u postgres pgbackrest --stanza=main --delta restore
sudo pg_ctlcluster 18 main start
Moving rather than deleting matters: if the second restore fails too, you still have the evidence. And verify is the cheap check that catches a corrupt page on a Tuesday afternoon instead of during a cutover.
Step 7: Tune the new object-store prefetch while you are in here
The other 2.60.0 feature worth configuring is prefetching from object stores. Requests are now queued ahead of the current read, and small gaps between files are read over instead of starting a new request. Restore uses it for bundled files and block incremental maps; backup uses it for prior block incremental maps.
[global]
repo-storage-prefetch=3 # default; raise for high-latency S3/Azure reads
repo-storage-read-over=64KiB # default; largest gap to read over
On S3 or Azure with real latency, raising repo-storage-prefetch is a low-risk win for restore duration. Measure it with --log-level-console=detail on a restore into staging rather than tuning it from an article, because the right value depends on object size, latency, and process-max. GCS users also get Workload Identity Federation in this release via repo-gcs-key-type=web-id, which takes long-lived service account keys out of the backup path.
The takeaway for this upgrade window
2.60.0 does not create corruption — it stops hiding it. The release turns a silent restore into a loud one, so the actual work is preparation: confirm checksums are enabled, run a full backup and read the new warning count, map any warnings to relations, and rehearse a restore you deliberately let fail so the runbook holds up under pressure.
If you would rather have a second pair of eyes before your next upgrade window, we run Postgres backup and point-in-time recovery reviews — including reading your pgBackRest configuration for exactly these failure modes.
More on this site: if you are still settling your strategy, start with PostgreSQL backup and restore, step by step, and if your restores have gone wrong around failovers before, read Postgres failover and archive_mode: why backups break.
Sources: pgBackRest 2.60.0 release announcement, 2.60.0 release notes, pgBackRest configuration reference.
