DEPLOY.PREFLIGHT.INACTIVE_REPLICATION_SLOT — A replication slot with no consumer is holding WAL
- Category: safety
- Severity: derived per finding from the slot's
wal_status— see the table below - Level: 0
- Downtime class:
onlineon the finding; the rule catalog names none for this check, as it does for every deploy check. The finding is about a state the instance is already in, not about a statement anybody is going to run — nothing here takes a lock or stops a read. - Stability: stable
- Suites: deploy
- Applies to: PostgreSQL. MySQL has no counterpart: binary-log retention there is a time-or-size
policy on the primary (
binlog_expire_logs_seconds), not a per-consumer reservation, so there is no object to name and no consumer to hold responsible for it.
What it reports
A row in pg_replication_slots that is inactive and still reserving: no consumer is connected,
and the slot's restart_lsn is set. One finding per slot, ordered by slot name.
-- what the check asks, reduced to its condition
select slot_name, slot_type, wal_status,
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) as retained_bytes
from pg_replication_slots
where not active and restart_lsn is not null
order by slot_name;
A slot is a promise: the primary keeps every WAL segment since the slot's restart_lsn until the
consumer confirms it. That is exactly right while somebody is consuming. When the consumer is gone —
a replica retired, a CDC connector switched off, a logical subscriber that failed and was never
cleaned up — the promise stands and the segments accumulate. Nothing errors. The disk fills.
active alone is not the condition, and that is the whole trap. A slot that reserves nothing is
holding nothing, and a freshly created slot waiting for its first consumer looks identical to a
retired one on active alone. Only restart_lsn tells the two apart, so the distinction is made in
the query rather than left to a reader.
The size comes from pg_current_wal_lsn() - restart_lsn rather than from safe_wal_size,
deliberately: safe_wal_size is null whenever max_slot_wal_keep_size is unset — which is the
default, and the most dangerous configuration, because retention is then unbounded. Reporting null
there would leave the worst case as the one with no number. The bytes are scaled to B, KB, MB, GB or
TB so they can be held against a disk instead of read as a ten-digit number, and a size that could
not be reported is named an unreported amount, never zero.
The finding names the slot itself, and the message carries what you act on: whether the slot is physical or logical, how much is being held, and what the server calls the state.
The four wal_status values, and what each one means here
The severity is picked per finding, because the four call for different actions. A reserved slot
is a housekeeping item; a lost one is a replica that has to be rebuilt, and a report that spelled
both inactive replication slot would have you treating the second like the first.
wal_status | Severity | What it means for this deploy |
|---|---|---|
reserved | low | Within max_slot_wal_keep_size, or no limit is set at all — which is the default, and means retention is unbounded. |
extended | medium | Already past max_slot_wal_keep_size; the segments are being kept anyway, for now. |
unreserved | high | The segments are no longer guaranteed. The consumer has to catch up soon or it will no longer be able to. |
lost | high | The segments are gone. Whatever was reading from this slot cannot resume and has to be rebuilt. |
lost is the one that reads as the least urgent and is the most final: the damage is done, and the
slot is now retention with nothing left to show for it.
A wal_status this build does not recognize is reported at medium, with the message saying so
in as many words — a gap in SQLens rather than in your database. The slot and its size still
stand; only the interpretation is missing.
Why a deploy is the moment to say so
A migration that rewrites a table needs room for a second copy of it while the rewrite runs, and every rewrite generates WAL in proportion to what it touches. A retained slot has already spent some of the headroom that rewrite is about to ask for.
That is not the whole argument, though. The rest is that a retained slot is the one cause of a full
disk the instance can name. DEPLOY.PREFLIGHT.DISK_HEADROOM reports undetermined on a managed
database, because free capacity is not something the server exposes — so on exactly the instances
most projects run on, will this fit has no answer. A retained slot is visible from inside the
database, and it is the commonest reason the answer would have been no. That is why this check is
worth its query, and why it is reported before the headroom check rather than after it.
What to do about it
Decide what that consumer was, then act on the decision — do not start with the drop.
- Find out what was reading from the slot. The name and the slot type are the clues the finding can hand you — a physical slot was a streaming replica or a WAL receiver, a logical one a subscriber or a CDC connector. The rest is the part only a person outside the database can supply.
- Then either reconnect it or drop the slot deliberately. Reconnecting is what a replica that was down for maintenance needs. Dropping is what a consumer that is never coming back deserves — and it is only correct once you know which of the two you have.
SQLens does not drop the slot. Dropping one detaches its consumer permanently: a replica that was merely offline for maintenance cannot resume afterwards and has to be rebuilt from a base backup. The finding names the slot and what it is holding; the decision belongs to whoever knows what that consumer was.
When it cannot answer
If the view cannot be read, the check reports undetermined with the reason
replication_slots_unreadable and the server's own message attached. A managed database commonly
withholds this view.
That is not a pass, and the distinction matters here more than usual: not being able to see the slots is not the same as there being none, and whether a retired consumer has already spent the headroom this migration needs is then simply unknown.
What it does not claim
- It does not say the slot should go. An inactive slot is not always abandoned: a replica that is down for maintenance looks identical to one that will never return, and only a human knows which. The finding reports the state; it never prescribes the drop.
- No fixed severity. The severity rises with how far the slot has fallen behind, so the rule catalog names none — the finding carries the one that fits what was read.
- It measures what a slot is holding, not what is left. How much room the migration actually
needs, and whether there is that much, is
DEPLOY.PREFLIGHT.DISK_HEADROOM's question — and on a managed database that one has no answer. - A
wal_statusthis build does not know is a gap in SQLens, not a finding about your database. The slot and its size are still reported. - Active slots and slots that reserve nothing are out of scope by construction. A slot whose
consumer is connected, and a fresh slot with no
restart_lsnyet, produce no finding at all. - It is a reading, not a subscription. The slot is inspected once, in the moment
sqlens:predeployruns immediately beforemigrate --force. A consumer that disconnects a second later is not in this report.