Postgres Logical Replication Slots: Why They Are Disk-Fill Bombs Waiting for Forgotten Consumers

Replication slots solve a real problem and create a worse one. The mechanism keeps WAL files around as long as a consumer might need them, which is correct behavior, but a consumer that stops consuming becomes a slow-motion disk-fill catastrophe that the default monitoring catches only after th

Replication slots are one of those Postgres features whose default behavior is correct for one failure mode and catastrophic for another. The mechanism is conceptually simple: a slot is a server-side bookmark that promises to retain WAL files until the consumer named by the slot has confirmed it has applied or copied them. The promise is what makes streaming replication and logical replication and CDC tools work reliably, because it removes the timing dependency where a slow consumer could miss WAL that the primary had already recycled.

The same promise is what makes slots dangerous. A slot whose consumer disappears does not time out. The slot keeps the WAL forever, or until the disk fills, whichever comes first. The mechanism that protects a temporarily-slow consumer from data loss is the same mechanism that turns a forgotten test slot into a midnight pager event.

The chronology of a disk-fill incident

The pattern is predictable enough to describe in past tense. A team sets up a logical replication slot for a development environment or a one-off data migration or an experiment with a CDC tool. The consumer either fails or finishes its work and disconnects without dropping the slot. The slot sits in pg_replication_slots, marked inactive, retaining WAL.

For days or weeks, nothing notices. WAL accumulates in pg_wal at whatever the write rate of the database happens to be. A busy OLTP system might generate 10-50 GB of WAL per day; a logging-heavy workload can produce hundreds. Standard monitoring watches the database for query latency and connection counts and table bloat, none of which are affected. Disk-usage monitoring picks it up eventually, but by the time someone investigates, the disk is at 80 or 90 percent.

The intervention is to drop the slot, which releases the WAL retention obligation, which lets the next checkpoint recycle the accumulated files. The intervention is also irreversible from the consumer's perspective: any consumer that had been planning to come back and consume the slot has lost its position and must start over from a current snapshot.

What changed in Postgres 13

Postgres 13 added max_slot_wal_keep_size as a safety net. The parameter caps how much WAL any slot can pin. When a slot exceeds the cap, the WAL files past the cap are recycled and the slot is marked invalid. The slot still exists but its consumer can no longer use it without resetting, which is the same intervention that the manual drop produces but applied automatically before the disk fills.

The default for max_slot_wal_keep_size is -1, meaning unlimited, which preserves the pre-13 behavior. The reasonable production setting is a value that represents disk space the cluster can afford to lose to a runaway slot without affecting other operations: 50 GB on a small cluster, 200 GB on a medium one, larger on systems with substantial WAL volume and substantial disk headroom. The value is workload-dependent but the right way to think about it is "how much WAL is acceptable to retain before declaring the consumer dead and reclaiming the space."

The monitoring discipline

The view pg_replication_slots exposes the relevant columns: slot_name, active, restart_lsn, and the calculated lag in bytes. The everyday query is:

SELECT slot_name, active,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;

The output shows every slot, whether its consumer is currently connected, and how many bytes of WAL it is pinning. The alert thresholds depend on cluster size but useful ones are 5 GB as a yellow signal worth looking at within hours, 25 GB as a red signal worth paging on, and 50 GB as a critical signal that requires intervention regardless of business hours. The thresholds should be substantially smaller than max_slot_wal_keep_size so the alert fires before the automatic invalidation does.

The lifecycle discipline

Slots are easier to create than to track. A small team accumulates them over time as people experiment with replication or set up one-off feeds. The discipline that prevents the accumulation is metadata tagging combined with periodic review.

The metadata tagging convention is to include a creator name and a creation date in the slot name itself: cdc_alice_20260301 rather than cdc_main. The convention makes ownership visible at a glance and lets a quarterly review identify slots whose owners have left or whose creation dates are old enough that the slot's purpose is likely forgotten. The review is mechanical: anything older than 90 days without a documented owner gets dropped after a warning to recent contributors.

The other practice that helps is the temporary slot variant, available since Postgres 10. A temporary slot is automatically dropped when its creator's connection closes. The variant is right for short-lived consumers like ad-hoc pg_recvlogical sessions or experimental CDC tools where forgetting to clean up is more likely than needing the slot to survive a disconnect. Production logical replication needs persistent slots because subscribers reconnect; experiments do not.

What the failure mode tells us about database design

Replication slots are an instance of a broader pattern: database features that work indefinitely in the happy path are usually the ones that fail catastrophically in the unhappy path. The features that work for the first 99 percent of cases without operator attention are the same features that produce a memorable on-call incident the first time something goes wrong.

The pattern shows up across the database: autovacuum that quietly falls behind a write-heavy table until wraparound, indexes that accumulate dead tuples until query plans flip, connection pools that hide a slow consumer until traffic spikes. The remediation in each case is the same: monitor the underlying state, alert well below the catastrophic threshold, and treat the absence of immediate failure as insufficient evidence that the system is healthy.

The SQLite analogue

Our four products run on SQLite, where the slot problem does not exist because SQLite has no replication infrastructure. The analogous problem is the WAL file growing unbounded when a long-running read transaction prevents checkpoint completion, which has the same shape: a feature that holds resources for a consumer that may not come back, with no built-in timeout.

The Postgres migration plan for our products includes replication-slot monitoring as a launch requirement. The list of monitors we plan to add to our standard observability pack is short: slot count, slot age in bytes, slot active state, and max_slot_wal_keep_size as a tunable safety net. The cost of adding the monitoring is small. The cost of not having it the first time a slot is forgotten is whatever the cluster's disk-full failure mode looks like, which is usually unavailable writes followed by a manual recovery procedure that takes longer than anyone wants to think about.


Our products: DocuMint (PDF invoice generation API), CronPing (cron job monitoring with status pages), FlagBit (feature flags API for modern teams), and WebhookVault (webhook capture and replay) put these patterns into production.

Read more