max_slot_wal_keep_size decides who dies when a replica stops consuming WAL: the replica, or the primary. The default picks the primary.
That is not an exaggeration, and it is the whole argument for the parameter. A replication slot’s job is to keep the primary from recycling WAL its consumer hasn’t received yet, and a slot keeps that promise whether the consumer is a standby that fell behind for ten seconds or a subscriber whose apply worker has been dying on a missing table since Tuesday. Before 13 there was no upper bound at all: pg_wal grew until the volume filled, and then the primary went down, taking the replica it had been so carefully protecting with it. Kyotaro Horiguchi’s patch to cap it started in 2017 and landed in 13 under Álvaro Herrera, and its design principle deserves stating plainly, because the default contradicts it: it is better to kill a replica than the primary that feeds it.
The default is -1, meaning no limit. The context is sighup, so you can set it without a restart, and the unit is megabytes; it arrived in 13 in the same release that renamed wal_keep_segments to wal_keep_size. The value is a distance, not a total: at every checkpoint, PostgreSQL takes the oldest restart_lsn among all slots, and if that point is more than this many megabytes behind the current write position, the retention horizon is pulled forward to exactly this far back and any slot whose restart_lsn is behind it is invalidated. WAL is retained once, not once per slot, so ten slots cost the same disk as one; what the parameter bounds is how far behind the slowest one may fall.
What invalidation looks like
The checkpointer does it, and it logs what it did. For an idle slot:
1 LOG: invalidating obsolete replication slot "sub"
2 DETAIL: The slot's restart_lsn 0/617AC38 exceeds the limit by 15225800 bytes.
3 HINT: You might need to increase "max_slot_wal_keep_size".
For a slot with a walsender attached, the checkpointer first terminates the walsender (terminating process 277 to release replication slot "stall"; the client sees terminating connection due to administrator command), then invalidates the slot and removes the WAL. Either way the slot is left in pg_replication_slots with wal_status = 'lost', invalidation_reason = 'wal_removed' (17 and later), a NULL restart_lsn, and an inactive_since frozen at the moment it died. It still occupies one of your max_replication_slots. Anything that tries to use it gets:
1 ERROR: can no longer access replication slot "sub"
2 DETAIL: This replication slot has been invalidated due to "wal_removed".
(That is the 18 wording; older releases say the slot “exceeded the maximum reserved size”, which is the same thing.) A standby whose physical slot has been invalidated logs that every wal_retrieve_retry_interval and gets nowhere, unless it has a restore_command and the WAL is in the archive, in which case it keeps up from the archive; streaming comes back once you drop the lost slot and create a fresh one, which for a physical slot is one function call. Without an archive, it is a re-clone. A logical subscriber is worse off: a logical slot cannot be recreated at an old position, so the subscription has to be dropped and recreated, which means a fresh initial copy of every table. That asymmetry is the main input to the number you choose.
pg_replication_slots.wal_status tells you where each slot stands. reserved means the slot’s WAL is inside what the last checkpoint would keep anyway (roughly max_wal_size worth); extended means beyond that, but still under this limit or under wal_keep_size; unreserved means the limit is exceeded and the next checkpoint will act; lost is after. safe_wal_size is the number of bytes that can still be written before the slot is in danger, and it goes negative in unreserved. It is NULL while the parameter is -1, which is the quieter cost of the default: with no limit there is nothing to measure against, so the one column that would tell you how close a stalled slot is to filling the disk has nothing to say.
Here is the sequence on 18.6, with max_wal_size = 48MB, this parameter at 96MB, and a subscriber to a FOR ALL TABLES publication that was missing one of the tables. The apply worker failed on the first transaction that touched it, restarted, failed again, and kept doing that every five seconds; the slot stopped advancing at the first failure. wal_status went from reserved to extended at 92 MB behind, and the first checkpoint after 110 MB invalidated the slot. On that machine the whole thing, from the apply worker’s first error to the slot’s death, took about a second, because generating 100 MB of WAL takes about a second. A broken subscriber is not a slow consumer. It is a stopped one, and the only question is how much disk it gets to take with it.
The details that change the number
A few things about the arithmetic matter when you pick a value.
wal_keep_size is a floor, not a competitor. WAL kept for wal_keep_size is kept for everyone, slots included, and a slot is only invalidated if its restart_lsn is behind the horizon after wal_keep_size has been applied. On 18.6 with wal_keep_size = 256MB and this parameter at 96MB, a slot 184 MB behind sat at extended through a checkpoint; the moment wal_keep_size was reset, the next checkpoint invalidated it. If both are set, the effective slot limit is the larger.
The unit is megabytes but the resolution is a WAL segment. The value is converted to whole segments by integer division, so anything under wal_segment_size (16 MB unless you changed it) rounds down to 0, and 0 means a slot gets no retention beyond what the checkpoint keeps for its own crash recovery: at each checkpoint, any slot whose restart_lsn is behind the checkpoint’s redo pointer is invalidated. 0 is not “off”; -1 is the only spelling of “unlimited”.
Enforcement is at checkpoint, so the real ceiling on pg_wal is this value plus whatever accumulates between checkpoints, plus the checkpoint’s own retention. Budget the parameter against the volume with that slack in mind, not as the exact high-water mark.
The limit is suspended during pg_upgrade, so the logical slots it migrates from 17 onward can’t be invalidated out from under it. And it is a limit on WAL only. A slot also pins an xmin (and, for logical slots, a catalog xmin), and this parameter does nothing about that; a dead subscriber will hold back vacuum on the primary’s catalogs indefinitely with this parameter set, right up until the slot is invalidated for WAL. Time-based invalidation of idle slots is idle_replication_slot_timeout, new in 18, and the two belong together.
Standbys run the same logic at restartpoints against their own slots, which matters if anything replicates from a standby.
Setting it
Set it on every primary, and on every standby that has its own downstreams. -1 is the correct value for a database whose replicas are all disposable and whose disk you don’t mind losing, which describes nobody in production.
The number is the amount of pg_wal you would rather spend than lose the slowest consumer. The floor is a few hours of peak WAL generation, so that a subscriber restart, a network partition, or a long-running apply transaction doesn’t get a replica killed for a problem that would have fixed itself. The ceiling is the free space on the pg_wal volume minus max_wal_size and a margin for the between-checkpoint slack. Logical subscribers get a value toward the top of that range, because their rebuild is a full re-copy rather than a re-clone; physical standbys with a WAL archive can sit near the floor, because for them invalidation is a detour through restore_command and a new slot rather than a rebuild. On the systems I see, that works out to somewhere between 20 GB and 200 GB, and the exact figure matters much less than having one.
Then alert on the gauge the setting turns on:
1 SELECT slot_name, active, wal_status,
2 pg_size_pretty(safe_wal_size) AS safe,
3 pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS behind,
4 invalidation_reason
5 FROM pg_replication_slots;
Page when wal_status leaves reserved, because extended is a slot that has already stopped keeping up and unreserved is one with a checkpoint’s worth of life left. Treat lost as a slot to drop and a consumer to rebuild, in that order. The parameter’s job was to make sure that, when the choice came, the thing that died was the replica.