Skip to main content
RunBook Academy

← All break/fix scenarios in PostgreSQL

advancedpg-replication-slot~40 min

A replica was decommissioned in March and took the primary down in June

Reported symptoms

  • db-prod-11 PANICs at 04:52 with could not write to file pg_wal/xlogtemp.2214: No space left on device and the postmaster shuts down
  • The cluster does not come back: the startup process fails with the same No space left on device and the server exits
  • The filesystem holding the data directory is 100 per cent full, and the largest directory in it by a wide margin is pg_wal
  • Row counts and table sizes are unchanged from a month ago and the application has had no unusual write volume
  • The only recent change on the host is an operating system patch applied five days earlier
  • A search of the change calendar finds nothing relevant in the preceding week, and the team begins investigating the patch
  • The decommissioning of a read replica eleven weeks earlier is not initially connected to the incident by anybody

Evidence

  • · pg_wal contains 44118 segments totalling 689 gigabytes against a max_wal_size of 8 gigabytes
  • · The archive_status directory contains no .ready files, and pg_stat_archiver reports failed_count of zero with last_archived_time within the last minute before the PANIC, so archiving is healthy
  • · pg_replication_slots, read from a copy of the cluster started with a temporary tablespace on another filesystem, lists a slot named replica_analytics with active false and a restart_lsn from 12 March
  • · The same view shows wal_status reserved and a null safe_wal_size, because max_slot_wal_keep_size is -1 on this cluster
  • · pg_stat_replication is empty and has been for eleven weeks according to the monitoring history
  • · The change record for CHG-2209, closed on 12 March, describes decommissioning the analytics read replica and lists deleting the VM, removing the monitoring entry, and updating the runbook
  • · The change record contains no step for dropping the replication slot on the primary
  • · A second production cluster in the same estate carries two inactive slots with restart_lsn values from January, currently retaining 71 gigabytes between them
Diagnosis and resolutionclick to reveal

Root cause

A physical replication slot outlived the standby that created it. The primary honoured that slot for eleven weeks, retaining every WAL segment from the slot's `restart_lsn` onwards, until the filesystem was full and the cluster PANICked. A slot is a promise. It tells the primary "keep every WAL segment from this position onwards, because I will be back for it", and the primary keeps that promise absolutely — through restarts, indefinitely — because it has no way to distinguish a standby that is offline for ten minutes of maintenance from one whose VM was deleted in March. The retention is unconditional in a way that defeats every ordinary remedy. A checkpoint cannot remove a segment at or after any slot's `restart_lsn`, so `max_wal_size` was unreachable in the same way it is when archiving fails. Reducing `max_wal_size` would not have helped. Increasing checkpoint frequency would not have helped. Archiving was healthy throughout and was irrelevant. `max_slot_wal_keep_size` is `-1` on this cluster, which is the default and means unlimited. That default encodes a specific choice: **the primary will fill its disk rather than break a standby.** It is a defensible choice for a cluster whose standbys matter more than its disk headroom, and it is the wrong one for a cluster where a slot can be orphaned and nobody notices. Nobody had made the choice; it had been inherited. The decommissioning change removed the VM, the monitoring entry and the runbook section. It did not drop the slot, and once the monitoring entry was gone there was nothing left that referred to the replica at all. The slot became invisible on the same day it became dangerous.

Remediation

The cluster is down and the filesystem is full, so the first task is to get enough space to start it rather than to fix the cause. Do **not** delete files from `pg_wal`. Segments retained by a slot are still required by that slot, and while the slot is about to be dropped anyway, deleting WAL by hand on a cluster that will not start is how a recoverable incident becomes a restore from backup. The safe route to space is to move `pg_wal` to a filesystem that has room and symlink it back, which is a supported layout: ```bash systemctl stop postgresql@18-main # already down, but be certain mv "$PGDATA/pg_wal" /mnt/spare/pg_wal ln -s /mnt/spare/pg_wal "$PGDATA/pg_wal" chown -h postgres:postgres "$PGDATA/pg_wal" systemctl start postgresql@18-main ``` Start the cluster and confirm it reaches "database system is ready to accept connections" in the log rather than trusting the start command's exit status. Then find the slot and confirm it has no consumer: ```sql SELECT slot_name, slot_type, active, active_pid, restart_lsn, wal_status, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained FROM pg_replication_slots ORDER BY slot_name; ``` Confirm against `pg_stat_replication` that nothing is streaming from it, and confirm against the estate inventory that the standby genuinely no longer exists. Dropping a slot whose standby is merely offline for maintenance destroys that standby. Then drop it: ```sql SELECT pg_drop_replication_slot('replica_analytics'); ``` Checkpoint, and watch `pg_wal` shrink over the following checkpoints — gradually, because segments are recycled by renaming rather than deleted in one pass. Finally, audit every other cluster in the estate for inactive slots before closing the incident. The evidence already shows a second cluster carrying 71 GB in two of them.

Verification

The cluster is running and the log's final line is "database system is ready to accept connections". `SELECT slot_name, active FROM pg_replication_slots` shows no inactive slots, or only ones with a documented, currently-offline consumer. `du -sh $PGDATA/pg_wal` falls toward `max_wal_size` across successive checkpoints. It will not fall immediately and that is expected. The filesystem has recovered enough headroom that the temporary `pg_wal` relocation can be reversed during a planned window, and that reversal is scheduled rather than left as a permanent undocumented layout. `SHOW max_slot_wal_keep_size` returns a deliberate value, and the reasoning behind that value is recorded. An estate-wide query confirms no other cluster carries an inactive slot.

Prevention

**Alert on `NOT active` in `pg_replication_slots`.** This is the early signal and it fires the moment a consumer disconnects, eleven weeks before anything runs out of space. An inactive slot always resolves to one of two things — somebody is bringing the consumer back today, or the slot should be dropped — and both are answerable in minutes. **Set `max_slot_wal_keep_size` deliberately.** The default of `-1` means the primary will fill its disk rather than break a standby. Choose from your own arithmetic: peak WAL rate times the longest a standby may plausibly be offline. Then alert on `safe_wal_size` falling, which gives you the entire headroom as warning time where disk usage gives you the last few percent. Understand what you are choosing. A bounded slot protects the primary and destroys the standby when the bound is exceeded, which then requires a full rebuild. That is usually the right trade, and it must be a decision rather than a default. **Put "drop the replication slot" in the decommissioning runbook**, next to deleting the VM. This is the specific missing step, and it is missing because the slot lives on a different server from the thing being decommissioned. **Audit slots on a schedule.** A quarterly query across the estate for inactive slots costs nothing and catches every instance of this that the alerting missed. **Graph `pg_wal` size against `max_wal_size`.** A ratio persistently above one has exactly two causes — a failing archive or a retaining slot — and both deserve a page long before the filesystem is involved.

Reported symptoms

db-prod-11 PANICs at 04:52:

PANIC:  could not write to file "pg_wal/xlogtemp.2214": No space left on device

The postmaster shuts down. The cluster does not come back — the startup process fails with the same error and exits.

The filesystem holding the data directory is 100% full. The largest directory in it, by a very wide margin, is pg_wal. Row counts and table sizes are unchanged from a month ago and there has been no unusual write volume.

The only recent change on the host is an OS patch five days earlier. The change calendar has nothing else in the preceding week, and the team starts investigating the patch.

Nobody connects the incident to a read replica that was decommissioned eleven weeks earlier.

Evidence provided

Read-only / Safe689 gigabytes against an 8 gigabyte target
$ du -sh $PGDATA/pg_wal && ls $PGDATA/pg_wal | grep -c '^0000'
689G	/var/lib/postgresql/18/main/pg_wal
44118

Illustrative output

Archiving is healthy: no .ready files, failed_count zero, last_archived_time within a minute of the PANIC. That eliminates the other cause of unbounded WAL retention.

Once the cluster is started with pg_wal relocated:

Read-only / Safea slot with no consumer, holding a position from March
$ psql -c "SELECT slot_name, active, restart_lsn, wal_status, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained FROM pg_replication_slots;"
     slot_name     | active | restart_lsn  | wal_status |  retained  
-------------------+--------+--------------+------------+------------
replica_analytics | f      | 8C/1A004628  | reserved   | 689 GB

Illustrative output

pg_stat_replication is empty and has been for eleven weeks according to the monitoring history. max_slot_wal_keep_size is -1.

Change record CHG-2209, closed 12 March, describes decommissioning the analytics read replica. Its steps are: delete the VM, remove the monitoring entry, update the runbook. There is no step for the slot.

A second production cluster in the same estate carries two inactive slots with restart_lsn values from January, currently retaining 71 GB.

Work the evidence before reading on

  1. pg_wal is 86 times max_wal_size and archiving is healthy. What is the remaining cause?
  2. The slot’s restart_lsn is dated 12 March. What is significant about that date?
  3. max_slot_wal_keep_size is -1. What has the server been instructed to prioritise?
  4. The decommissioning change removed the VM and the monitoring entry. Why did that make the problem harder to find rather than easier?

Root cause

The slot outlived the standby, and the primary kept its promise

A replication slot records a position and instructs the primary to retain every WAL segment from that position onwards. The primary honours it unconditionally — across restarts, indefinitely — because it cannot distinguish a standby offline for ten minutes from one deleted in March.

The default made the choice

max_slot_wal_keep_size = -1 means unlimited retention, and it encodes a real decision: the primary will fill its disk rather than break a standby.

That is defensible where standbys matter more than headroom. It is the wrong choice where a slot can be orphaned and nobody notices. What is not defensible is having neither made the choice nor known it was made.

Removing the monitoring entry removed the last reference

The decommissioning change deleted the VM, removed the monitoring entry, and updated the runbook. Every one of those steps removed something that referred to the replica.

The slot was on a different server, was not referred to by any of them, and became invisible on exactly the day it became dangerous. Eleven weeks later there was no artefact anywhere in the estate connecting replica_analytics to anything a human would search for.

Resolution

The cluster is down with a full filesystem. Get space first, cause second.

Relocate pg_wal to a filesystem with room. This is a supported layout and it is reversible:

systemctl stop postgresql@18-main
mv "$PGDATA/pg_wal" /mnt/spare/pg_wal
ln -s /mnt/spare/pg_wal "$PGDATA/pg_wal"
chown -h postgres:postgres "$PGDATA/pg_wal"
systemctl start postgresql@18-main
tail -3 /var/log/postgresql/postgresql-18-main.log

Confirm from the log that the server reached database system is ready to accept connections.

Find the slot, and confirm it genuinely has no consumer:

SELECT slot_name, slot_type, active, active_pid, restart_lsn, wal_status,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots ORDER BY slot_name;

SELECT count(*) FROM pg_stat_replication;
SELECT pg_drop_replication_slot('replica_analytics');
CHECKPOINT;

pg_wal shrinks over the following checkpoints rather than at once, because segments are recycled by renaming.

Before closing the incident, audit the estate. The evidence already shows 71 GB held on another cluster.

Verification

The cluster is running and the log confirms it.

No inactive slots remain, or only ones with a documented, currently offline consumer.

pg_wal falls toward max_wal_size across successive checkpoints.

The pg_wal relocation is scheduled to be reversed in a planned window rather than left as an undocumented permanent layout.

SHOW max_slot_wal_keep_size returns a value somebody chose, with the reasoning recorded.

Prevention

Alert on NOT active in pg_replication_slots. This is the eleven weeks of warning that existed and was not used:

SELECT slot_name, active, wal_status, invalidation_reason,
       pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS retained_bytes,
       safe_wal_size
FROM pg_replication_slots
WHERE NOT active OR wal_status <> 'reserved';

Set max_slot_wal_keep_size deliberately, and alert on safe_wal_size falling.

Add the slot drop to the decommissioning runbook, next to deleting the VM. It is missing precisely because the slot lives on a different server from the thing being retired.

Audit slots quarterly across the estate.

Graph pg_wal size against max_wal_size. A ratio persistently above one has two causes and both deserve a page long before the filesystem is involved.