WAL retention due to replication slot overflow

Database Replication Slot Overflow Fix

Database Errors Intermediate 👁 10 views 📅 Jun 17, 2026

When a replica disconnects or falls behind, its replication slot fills up. This blocks WAL cleanup and fills your disk. Here's how to clear it.

When This Error Hits

You see this error when a PostgreSQL replica disconnects for hours or days—usually during a maintenance window or network blip. The primary keeps accumulating Write Ahead Log (WAL) files because the slot thinks the replica still needs them. Your disk fills up. Suddenly. The database goes read-only. Panic sets in.

I've seen this most often on PostgreSQL 14 and 15 systems where wal_level = logical is set for CDC tools like Debezium or AWS DMS. A single dangling slot can eat 200 GB of WAL in 24 hours on a busy system.

Root Cause in Plain English

Every replication slot remembers the last WAL position the replica acknowledged. If the replica stops talking—crashed, network down, or just slow—the slot's restart_lsn stops advancing. PostgreSQL refuses to recycle any WAL beyond that point. The pg_wal directory swells until it chokes the disk.

There's no automatic timeout for slots. The database assumes the replica will come back. That assumption is wrong more often than you'd think.

The Fix: Drop the Stale Slot

This is the nuclear option, and it works. The replica will re-sync from scratch, but you get your disk back in minutes.

  1. Identify the bloated slot. Connect to the primary and run:
SELECT slot_name, slot_type, active, restart_lsn, xmin
FROM pg_replication_slots
WHERE NOT active;

Look for slots where active = false and restart_lsn is far behind the current WAL insert position. That's the offender.

  1. Confirm which slot you're dropping. Double-check the name. Dropping the wrong slot will kill a good replica. On production, I run a quick SELECT pg_walfile_name(restart_lsn) FROM pg_replication_slots WHERE slot_name = 'your_slot'; to see exactly which WAL file it's pinning.
  2. Drop the slot.
SELECT pg_drop_replication_slot('your_slot_name');
  1. Force WAL cleanup (if needed). Normally PostgreSQL reclaims the space immediately after the drop. If not, run a checkpoint:
CHECKPOINT;

Then check disk space. On a healthy system, the WAL directory shrinks fast. I've seen 150 GB freed in under 30 seconds.

If the Disk Is Still Full After the Drop

Sometimes PostgreSQL doesn't release the WAL files right away because of replication slots on the same primary that are still active but lagging. Run the check from step 1 again—there might be multiple stale slots.

Another gotcha: if you're using wal_keep_segments or max_slot_wal_keep_size (PostgreSQL 13+), the drop won't help if the problem is just a high wal_keep_segments setting. That's a different issue—you'd need to reduce wal_keep_segments and wait for a manual checkpoint.

And one more thing: if the disk is truly 100% full, PostgreSQL may not even let you connect. You'll need to free space at the OS level first—delete some old WAL files manually (yes, risky, but desperate times). On Linux:

cd /var/lib/postgresql/14/main/pg_wal
ls -lt | head -20
rm -f 0000000100000000000000A0

Pick the oldest ones—the ones with the lowest hex number. Remove maybe 10 files. That gives you enough room to connect and drop the slot properly.

Preventing This in the Future

Three things I do on every new PostgreSQL deployment now:

  • Set a slot size limit. In PostgreSQL 13+, use max_slot_wal_keep_size = '20GB' in postgresql.conf. This caps how much WAL a single slot can pin. If the slot exceeds it, PostgreSQL forces the slot to be invalid (you'll need to recreate the replica).
  • Monitor pg_replication_slots with a cron job. Every 5 minutes, check for inactive slots and alert. I use a simple script that sends a Slack message if any slot is inactive for more than 10 minutes.
  • Use physical replication instead of logical if you don't need CDC. Physical slots are easier to recover—you just restart the replica and it catches up. Logical slots are the ones that cause real headaches.

That's it. The fix is one SQL command, but the real work is finding the right slot and deciding when to pull the trigger. Better to drop it fast than wait for the disk to fill and take down the whole cluster.

Was this solution helpful?