Reported symptoms
A disk alert fires at 91% on db-prod-07 at 02:40. Nine days earlier the
filesystem was at 44%. Nobody noticed the trend in between.
No table has grown. The sum of pg_total_relation_size across every
database is within 2% of its value a fortnight ago. The nightly logical
backup has reported success every night, including tonight.
The team deletes old application logs and reclaims 11 GB. Usage is back
at 91% within four hours. Somebody proposes VACUUM FULL on the two
largest tables, on the theory that this is bloat.
Forty minutes in, somebody runs du on the data directory and finds
pg_wal at 340 GB, against a max_wal_size of 4 GB.
By 06:15 the filesystem is at 97% and there is a proposal to delete the oldest WAL segments by hand.
Evidence provided
$ du -sh $PGDATA/pg_wal && ls $PGDATA/pg_wal/archive_status/*.ready | wc -l340G /var/lib/postgresql/18/main/pg_wal
21406Illustrative output
$ psql -c "SELECT archived_count, last_archived_wal, last_archived_time, failed_count, last_failed_wal, last_failed_time FROM pg_stat_archiver;" archived_count | last_archived_wal | last_archived_time | failed_count | last_failed_wal | last_failed_time
----------------+--------------------------+-------------------------------+--------------+--------------------------+------------------------------
4471209 | 0000000100004E210000003C | 2026-08-19 21:14:07.221115+00 | 118442 | 0000000100004E210000003D | 2026-08-28 06:12:55.03318+00Illustrative output
$ grep -E 'archive command failed|failed too many times' /var/log/postgresql/postgresql-18-main.log | tail -32026-08-28 06:12:53.028 UTC [1841] LOG: archive command failed with exit code 1
2026-08-28 06:12:54.031 UTC [1841] LOG: archive command failed with exit code 1
2026-08-28 06:12:55.033 UTC [1841] WARNING: archiving write-ahead log file "0000000100004E210000003D" failed too many times, will try again laterIllustrative output
archive_command is cp %p /mnt/wal-archive/%f. The mount is present
and is mounted read-only, remounted during a storage maintenance nine
days earlier. pg_replication_slots is empty.
The monitoring configuration has a disk-usage alert and a
backup-exit-status alert. It has nothing referencing pg_stat_archiver
or archive_status.
Work the evidence before reading on
- No table has grown and
pg_walis eighty-five timesmax_wal_size. What can make that happen, and what are the two candidates? - The nightly backup succeeded every night. Was that report wrong?
- The database has been completely healthy from the application’s point of view for nine days. What has it not been?
- Somebody wants to delete the oldest WAL segments. What exactly does that destroy?
Root cause
A segment marked .ready cannot be removed
When a WAL segment fills, the server creates
pg_wal/archive_status/<segment>.ready. The archiver runs
archive_command for it, and on exit status zero renames the marker
to .done. Only a .done segment may be recycled or deleted at a
checkpoint.
The storage maintenance remounted /mnt/wal-archive read-only. cp
began returning 1. The server retried, correctly and forever, and every
segment produced since then stayed .ready.
The backup report was true and irrelevant
The nightly pg_dump writes to a different filesystem. It succeeded. It
has no relationship to WAL archiving and could not have detected this.
That is worth stating plainly because “the backups are fine” was said several times during the incident and was correct each time. A logical dump is a point-in-time snapshot; the WAL archive is what provides recovery to any moment between dumps. They are different capabilities with different failure modes and different monitoring.
Resolution
Establish the time available first, because the failure mode is a PANIC:
df -h $PGDATA
du -sh $PGDATA/pg_wal
ls $PGDATA/pg_wal/archive_status/*.ready | wc -l
Fix the destination. Remount read-write, and confirm by writing a file
as postgres — the archiver does not run as root and a root-writable
mount proves nothing:
mount -o remount,rw /mnt/wal-archive
su - postgres -c 'touch /mnt/wal-archive/.probe && rm /mnt/wal-archive/.probe && echo writable'
Nothing further is required. The archiver retries continuously; as soon
as archive_command succeeds the backlog drains in order. Watch it:
watch -n5 'ls $PGDATA/pg_wal/archive_status/*.ready 2>/dev/null | wc -l'
Once drained, pg_wal shrinks over several checkpoints rather than at
once, because segments are recycled by renaming rather than deleted. A
still-large pg_wal immediately after the drain is not a failed repair;
check the .ready count, which is zero straight away.
Verification
The .ready count is zero or falling.
pg_stat_archiver shows last_archived_time within minutes and
failed_count no longer increasing — watch the rate, not the value,
since the counter is cumulative.
pg_wal falls toward max_wal_size over subsequent checkpoints.
The archive is continuous. Verify it rather than assuming, because a gap means the recovery window is still broken:
ls /mnt/wal-archive/ | grep -E '^[0-9A-F]{24}$' | sort | \
awk 'NR>1 && strtonum("0x" substr($0,17)) != prev+1 {print "GAP before " $0}
{prev=strtonum("0x" substr($0,17))}'
A recovery is performed. Restore a base backup on a separate host,
set recovery_target_time to a timestamp inside the affected window, and
confirm the recovery reaches it. Until that has been done, the recovery
capability is a claim.
Prevention
Alert on pg_stat_archiver. failed_count increasing, and
now() - last_archived_time above a threshold. Both fire within minutes
of the first failure.
Alert on the .ready count. It needs no database connection and
works when the database does not:
ls $PGDATA/pg_wal/archive_status/*.ready 2>/dev/null | wc -l
Replace the bare cp. It overwrites on retry, which silently
corrupts the archive; it can leave a truncated file; and it returns
before the data is durable. Use a real tool, or at minimum guard it and
write-then-rename.
Add the archive to the storage maintenance checklist. The remount was the proximate cause and nobody asked what depended on that export.
Rehearse the recovery monthly. A successful pg_dump says nothing
about point-in-time recovery. A restore to a timestamp would have found
this in days.
Alert on the recovery window, expressed as the age of the newest segment in the archive. That is the number the business is actually buying, and it silently went to nine days.