idle_replication_slot_timeout shares an alphabetical neighborhood with idle_in_transaction_session_timeout and idle_session_timeout, but it solves the opposite problem. Those disconnect clients that stay too long. This one, new in PostgreSQL 18, deals with clients that stop showing up at all: it puts an expiration date on replication slots.

A replication slot is an open-ended promise. As long as the slot exists, PostgreSQL retains every WAL segment its consumer might still need; for a logical slot, it also holds back the system catalog horizon (catalog_xmin) so that decoding can still make sense of old changes when the consumer returns. The promise is kept whether the consumer is a healthy standby thirty seconds behind or a Debezium proof-of-concept somebody abandoned in March. The abandoned case always ends the same way: pg_wal grows until the disk fills and the primary goes down. I have watched this exact sequence take out more than one production system; it is not a theoretical failure mode.

The default is 0, which disables the mechanism entirely, and which is also the only behavior any version before 18 offers. The context is sighup, so you can enable it with a reload. A bare number is read as seconds, though you will never write one; sensible values come with units, like 3d. (During the 18 betas the granularity was whole minutes, and a value of 30s quietly rounded down to zero, which is to say off. The unit was changed to seconds before release.)

The clock runs from pg_replication_slots.inactive_since, the timestamp recorded when whatever process held the slot released it. Enforcement is lazy: the checkpointer sweeps for expired slots when a checkpoint runs (a restartpoint, on a standby), so a slot can overstay its deadline by up to checkpoint_timeout, and a manual CHECKPOINT forces the sweep. The penalty is invalidation, not removal. The slot stays in pg_replication_slots with invalidation_reason set to idle_timeout and wal_status set to lost, the server logs “invalidating obsolete replication slot”, the retained WAL becomes removable, the catalog horizon advances, and the corpse sits there until you pg_drop_replication_slot() it yourself.

Two exemptions and one subtlety. Slots a standby is synchronizing from the primary (synced is true) are exempt, since they are idle by design; so are slots that have never reserved WAL, such as a physical slot created but not yet used. The subtlety: a restart resets the clock. inactive_since is reinitialized to the startup time for every slot, so bouncing the server hands each abandoned slot a fresh, full timeout.

Be clear about what invalidation costs, because this parameter exists to deliberately break the guarantee that slots provide. An invalidated slot cannot be used again. A logical subscriber has to be resynchronized, which for a subscription of any size means a new slot and recopying tables. A physical standby whose slot expired must catch up from the WAL archive or be rebuilt. That is the trade: the primary survives, and the consumer starts over. The value therefore needs to be comfortably longer than the longest consumer outage you intend to tolerate; a weekend of planned subscriber maintenance must not trip it. Days, not hours.

PostgreSQL 13 added the size-based version of this tripwire, max_slot_wal_keep_size, which caps retained WAL in bytes rather than time. They are complements, not alternatives. On a quiet system, a dead slot can pin catalog_xmin for a month without ever retaining enough WAL to trip a size cap; the time cap catches it. Conversely, a write storm can blow through a size budget in an hour while the consumer is merely slow, and the time cap will not notice. On 18, run both.

So: turn it on. 3d is a reasonable opening bid; long enough that no planned maintenance ever comes close, short enough that a forgotten slot dies before it takes you with it. Then alert on the age of inactive_since, because the correct number of times this parameter fires in production is zero. It is the backstop for the week nobody was watching.