What this runbook covers
You run Postgres with a logical-decoding CDC connector (this page uses Debezium's Postgres connector as the example). The disk on the primary is filling, or an alert says it might, and you need to know whether a replication slot is the cause, what each fix costs, and what the pipeline owes you afterwards.
The thread through all of it is the same as the rest of this site. A slot is a position held on the source. The consumer moves that position forward only by confirming it has received changes, so delivery is at-least-once. Anything that gives up or loses the position (a drop, an invalidation) means the only correct way back is a re-snapshot into a sink that is idempotent: keyed by primary key and ordered by log position. There is no end-to-end exactly-once at any point in this runbook.
Triage: is a slot why the disk is filling?
Connect to the primary (not a replica) and look at every slot, logical and
physical. pg_current_wal_lsn() cannot run during recovery, so
on a standby use pg_last_wal_replay_lsn() in its place.
-- Every slot, biggest WAL pin first. Needs PostgreSQL 13+ (wal_status, safe_wal_size).
-- A lost slot pins nothing, so its restart_lag is shown as NULL and it sorts last.
SELECT slot_name,
slot_type,
plugin,
database,
active,
active_pid,
wal_status,
pg_size_pretty(safe_wal_size) AS safe_wal_size,
pg_size_pretty(CASE WHEN wal_status <> 'lost'
THEN pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) END) AS restart_lag,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS unconfirmed,
restart_lsn,
confirmed_flush_lsn
FROM pg_replication_slots
ORDER BY CASE WHEN wal_status <> 'lost'
THEN pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) END DESC NULLS LAST;
| Column | What it tells you |
|---|---|
active, active_pid |
true means a session is streaming from the slot right
now, and active_pid is its process. false
with a NULL pid means nobody is connected. The slot keeps
pinning WAL anyway.
|
restart_lsn |
The oldest WAL that might still be required by the consumer. The
server will not remove WAL from here onward, unless a
max_slot_wal_keep_size limit is exceeded. NULL if no
WAL was ever reserved.
|
confirmed_flush_lsn |
How far the consumer has confirmed receiving. Data from transactions that committed before it is not available anymore from the slot. (Logical slots only; NULL for physical slots.) After a server crash the slot can still go back to an earlier position (see below). |
restart_lag |
Current WAL position minus restart_lsn (a byte count,
shown with pg_size_pretty in the query). For a slot that
is not lost, this is the WAL it makes the server keep:
an estimate of what the slot pins, not the size of
pg_wal. A lost slot pins nothing, whatever
this column would show, so the query returns NULL for it.
|
unconfirmed |
Current position minus confirmed_flush_lsn: how much
the consumer has not yet confirmed. Sample twice to separate a slow
consumer from a stopped one (see the decision table).
|
wal_status |
reserved: the WAL the slot needs is within
max_wal_size. extended: beyond
max_wal_size but still retained, either by the slot or
by wal_keep_size (so it is not proof of a slot problem
on its own). unreserved:
the slot no longer retains the WAL it needs and some will be removed
at the next checkpoint (it can return to reserved or
extended). lost: the slot is no longer
usable. For WAL retention, the last two states normally appear only
when max_slot_wal_keep_size is non-negative; newer
releases can also invalidate a slot for other reasons.
|
safe_wal_size |
Bytes that can still be written before the slot is in danger of
becoming lost. NULL for lost slots, and NULL whenever
max_slot_wal_keep_size is -1.
|
Careful with lost slots: a lost slot pins nothing, whatever
restart_lag would show. With
invalidation_reason = 'wal_removed' its
restart_lsn is NULL (observed, see the last section), but
with other reasons (for example idle_timeout on PostgreSQL
18) it can stay set. A NULL lag therefore means “this slot has
already failed”, not “nothing is retained by design”.
Read wal_status (and, on 17+, invalidation_reason)
before the lag columns.
Next, confirm the disk is really WAL, and look for the other thing a slot
pins. Reading pg_ls_waldir() needs a superuser or a role with
pg_monitor privileges.
-- Size of the WAL directory
SELECT pg_size_pretty(sum(size)) AS pg_wal_size FROM pg_ls_waldir();
-- The same slots also hold back VACUUM on catalog rows
SELECT slot_name,
age(xmin) AS xmin_age,
age(catalog_xmin) AS catalog_xmin_age
FROM pg_replication_slots;
-- PostgreSQL 17+ only: when did it go idle, and was it invalidated?
SELECT slot_name, inactive_since, invalidation_reason, failover, synced
FROM pg_replication_slots;
If pg_wal_size is far larger than max_wal_size
and a slot's restart_lag accounts for most of it, a slot is
the cause. If no slot explains it, look at WAL archiving and at physical
slots for standbys before blaming CDC (the first query lists those too).
How a logical slot holds WAL
A logical slot represents a stream of changes that can be replayed to a client in the order they were made. It lives on one database, persists independently of whichever connection is using it, and is crash-safe. Crucially it knows nothing about its consumer. Only one receiver can consume from it at a time, but the server does not care whether that receiver is alive.
-
The consumer connects and starts streaming. The server begins at the
greater of the position the client asks for and the slot's
confirmed_flush_lsn, so a client cannot read behind its own confirmed position. -
As it processes changes, the consumer sends status updates that report
the WAL position it has flushed. That is how
confirmed_flush_lsnmoves forward.restart_lsnfollows, possibly a little behind it, and that is what lets the server recycle older WAL. (From PostgreSQL internals rather than a docs sentence I can cite: in practice a long-running open transaction can holdrestart_lsnback, because decoding must be able to restart from the start of every transaction not yet decoded.) -
If the consumer stops confirming, nothing else moves. Both positions
stay where they were, and every new WAL file written from
restart_lsnonward is kept.
Two details matter for the rest of this page. First, the slot's current position is persisted only at checkpoints, so after a server crash the slot can return to an earlier position and recent changes are sent again; the PostgreSQL docs say logical decoding clients are responsible for handling the same message more than once. Second, the confirmation is about receipt by the consumer. It says nothing about whether the sink applied the change.
Why an unconsumed or inactive slot fills the disk
The PostgreSQL docs put it bluntly: replication slots persist across crashes and prevent removal of required resources even when there is no connection using them. The WAL is not recycled, so the disk fills at the rate the database writes. The usual routes into that state:
-
The connector is stopped, paused, crash-looping or deleted but
its slot remains. With Debezium's defaults the slot survives a
graceful stop (
slot.drop.on.stopisfalse), so a connector you shut down on Friday is still pinning WAL on Monday. -
The connector is running but slower than the write rate.
The slot is active,
unconfirmedkeeps growing, andrestart_lsntrails. - Most of the database's writes are not on captured tables. The connector reads the log as usual but rarely emits a change record, so no offset is committed and it never gets the chance to send the latest LSN back. The database keeps WAL for events the connector has already read. Debezium documents this case and the heartbeat fix below.
- The captured database is the quiet one in a busy cluster. Slots are per database, but WAL is shared by all databases in the cluster. If only another database is busy, the connector is never invoked, cannot confirm a position, and WAL grows until the captured database emits an event.
- Managed idle environments. Debezium's docs note that on AWS RDS for PostgreSQL, writes to RDS's own system tables that are invisible to clients can leave a nominally idle database in the same situation. Regular events solve it. Check your provider's behaviour.
- The slot is abandoned. A test connector, a retired pipeline, a trial of a managed ELT product. Nobody remembers it exists.
WAL is not the only cost. The same caution in the PostgreSQL docs says a
slot also prevents VACUUM from removing the catalog rows it
still needs, and that in extreme cases the database can shut down to
prevent transaction ID wraparound. That is why the triage includes
age(catalog_xmin).
Capping retention: max_slot_wal_keep_size
max_slot_wal_keep_size is the maximum size of WAL that
replication slots are allowed to retain in pg_wal at
checkpoint time. The default is -1, which means
unlimited: slots may retain any amount of WAL. A value
without units is read as megabytes. The setting exists in PostgreSQL 13 and
later and the docs for 13, 16, 17 and 18 describe it the same way.
With a limit set, if a slot's restart_lsn falls behind the
current position by more than the limit, the server stops protecting that
WAL. The slot moves through unreserved to lost,
and the consumer can no longer continue from it because the WAL it
needs has been removed. The docs word it as the consumer possibly
“no longer [being] able to continue replication due to removal of
required WAL files”. In PostgreSQL 17 and later
invalidation_reason says wal_removed.
What a limit trades
A limit trades disk for a lost slot. With no limit, a stuck consumer can fill the volume and stop the primary from writing, which takes the application down with it. With a limit, the primary survives, and the cost moves to the pipeline: the slot is invalidated and the only correct recovery is a re-snapshot. Both are real costs. A limit decides which one you pay.
So size it as policy, not as a number copied from a blog post: it should be
large enough to cover the longest consumer outage you intend to
survive at your peak WAL rate, and small enough that the volume still has
headroom for normal WAL, checkpoints and archiving. Both inputs
(write rate, disk size) are yours. Treat safe_wal_size as the
countdown to lost and alert well before it reaches zero.
Newer safety nets, by version
| PostgreSQL | What is new |
|---|---|
| 13+ |
max_slot_wal_keep_size, wal_status,
safe_wal_size.
|
| 17+ |
inactive_since and invalidation_reason in
pg_replication_slots; the failover and
synced columns; failover slots (see below).
|
| 18+ |
idle_replication_slot_timeout: invalidates a slot that
has been inactive longer than the duration. The default is zero,
which disables it, and invalidation happens at a checkpoint. It
shows up as invalidation_reason = 'idle_timeout'. It
only helps if you want an abandoned slot to be invalidated
automatically. An idle slot that is invalidated this way is lost,
with the same recovery as any other lost slot.
|
The docs say max_slot_wal_keep_size can only be set in
postgresql.conf or on the server command line. On PostgreSQL
18.3, pg_settings.context for it is sighup, which
means a reload applies it; check the same column on your version and on a
managed service, where the setting may live in a parameter group.
Debezium specifics
| Setting | Default | What it does for this problem |
|---|---|---|
slot.name |
debezium |
The slot the connector streams from; created automatically if
missing (the user needs the REPLICATION privilege, and
with pgoutput also CREATE on the
database). Every connector instance needs its own slot
name and its own publication. Debezium warns that if
connectors share a slot, each change reaches only one of them, with
no warning from Postgres. Names are lower-case letters, digits and
underscore.
|
slot.drop.on.stop |
false |
Drop the slot when the connector stops gracefully. Debezium says to
set it to true only in testing or development, because
dropping the slot lets the database discard WAL and the connector
then has to snapshot again or continue from a stored offset that the
new slot cannot honour. In production the default is what you want,
and the consequence is that decommissioning a connector
means dropping its slot yourself.
|
heartbeat.interval.ms |
0 (off) |
A positive value makes the connector emit heartbeat messages, which gives it the chance to send the latest LSN to the database so old WAL can be reclaimed. Needed when many writes happen in the database but few touch the captured tables. Debezium also says heartbeats can reduce the number of change events re-sent when the connector restarts. |
heartbeat.action.query |
none |
A statement the connector runs on the source at each heartbeat. For
a quiet database in a busy cluster, create a heartbeat table and
point this at an INSERT (or repeated UPDATE)
on it, so there are always captured changes to confirm. The
connector's database user must be able to run it.
|
publication.name, publication.autocreate.mode |
dbz_publication, all_tables |
With pgoutput, a publication defines which tables'
changes are sent. The heartbeat table must be in the
publication or the connector never sees the heartbeat
writes. If the publication predates your deployment and is not
FOR ALL TABLES, add it explicitly. In
filtered mode the connector rewrites the publication
from your include and exclude lists: Debezium documents
that it runs ALTER PUBLICATION <name> SET TABLE
for the tables matching those lists. So add
cdc_heartbeat to table.include.list, not
only to the publication. (That the manual
ADD TABLE would be undone at the next start is my
inference from the documented SET TABLE; I did not run
it.)
|
lsn.flush.mode |
connector |
Who confirms the LSN to the slot. manual means the
connector does not flush at all, and the docs warn WAL accumulates
unless you build something that does. Newer releases only (it
replaces flush.lsn.source), and
connector_and_driver is documented as Technology
Preview: check your version before relying on either.
|
-- Heartbeat table for a quiet database (the connector user needs rights on it)
CREATE TABLE IF NOT EXISTS cdc_heartbeat (id int PRIMARY KEY, beat timestamptz);
ALTER PUBLICATION dbz_publication ADD TABLE cdc_heartbeat; -- only for a publication that is not FOR ALL TABLES
-- With publication.autocreate.mode=filtered, also list public.cdc_heartbeat in table.include.list
{
"heartbeat.interval.ms": "10000",
"heartbeat.action.query": "INSERT INTO cdc_heartbeat (id, beat) VALUES (1, now()) ON CONFLICT (id) DO UPDATE SET beat = EXCLUDED.beat"
}
The interval above is an example, not a recommendation: pick it from how long you can tolerate WAL being pinned on a quiet database. The queries and snippets on this page were checked for syntax where noted in the last section; the connector JSON was not run against a live Debezium.
What happens to confirmed_flush_lsn while the connector is down
Nothing. It stays at the last position the connector confirmed, and
restart_lsn stays with it, so WAL accumulates for the whole
outage. Heartbeats do not help here, because there is no connector to send
them. When the connector restarts it asks for its stored offset, and the
server streams from the greater of that and the slot's
confirmed_flush_lsn. Changes the connector had already
published to Kafka but not yet confirmed come around again. Debezium
states this plainly: it provides at-least-once delivery and a recovering
connector might emit duplicates.
Failover and standbys
A slot lives on one server. What happens to it when the primary fails depends on the PostgreSQL version.
| Version | Behaviour |
|---|---|
| 15 and earlier |
Logical slots can only be created on the primary, and are not
propagated to replicas. After a failover the new primary has no
slot, so treat it as a lost slot: re-snapshot. Debezium's
snapshot.mode=always is documented for exactly the case
where a new primary was promoted.
|
| 16 | Logical slots can be created on a standby, but Debezium's docs say the standby's slot is not synchronised automatically and should be advanced manually to match the primary. |
| 17 and later |
A slot created with failover = true (the fifth argument
of pg_create_logical_replication_slot; Debezium's
slot.failover) can be synchronised to a standby, so the
consumer can continue from the new primary after promotion.
|
Failover slots are not a setting you flip on a slot. For synchronisation to work on PostgreSQL 17 and later, the PostgreSQL docs require:
-
sync_replication_slots = onon the standby (it is off by default), a physical slot between primary and standby named inprimary_slot_name,hot_standby_feedbackenabled, and a validdbnameinprimary_conninfo. -
Strongly recommended: list that physical slot in
synchronized_standby_slotson the primary, so a subscriber cannot consume changes faster than the standby. The price is latency: logical senders wait for the standby to confirm, and the primary will not shut down completely until the listed standbys confirm.
Caveats worth knowing before you trust it:
- The standby persists a synchronised slot only if the WAL and catalog rows it needs are still available on the standby. Otherwise it logs that synchronisation could lead to data loss and does not persist the slot. Temporary synchronised slots cannot be used for logical decoding.
-
Verify readiness before you rely on it. The PostgreSQL docs give this
check on the standby (
syncedand not temporary and not invalidated):SELECT slot_name, (synced AND NOT temporary AND invalidation_reason IS NULL) AS failover_ready FROM pg_replication_slots WHERE synced; - A slot with no active consumer will not advance, which stalls synchronisation; the docs suggest advancing it manually on the primary.
- If the old primary comes back during promotion, subscribers can keep talking to it. The docs recommend disabling subscriptions before promotion and re-enabling them once the connection string points at the new primary.
- Synchronisation is periodic. My reading, not a documented guarantee: the position on the promoted standby can be behind what the consumer had confirmed, so expect some replay after a failover. That is the same at-least-once rule, not a new failure mode.
- Debezium: do not drop or re-create a failover slot, and pause the connector until you have verified the slot is intact. Some managed services (Debezium names AWS RDS and Google Cloud SQL) replicate the slot with disk replication instead, so check what yours does.
Decision table: what is wrong, and what to do
Take two samples of the triage query, far enough apart that
pg_current_wal_lsn() has clearly advanced, and read them
against this table. Dropping a slot is the last row you should reach for,
not the first.
| Situation | How you tell | Recovery path |
|---|---|---|
| Lagging consumer |
active = true; confirmed_flush_lsn is
moving between samples but unconfirmed and
restart_lag are growing or not shrinking;
wal_status is reserved or
extended.
|
Fix throughput: the sink, the Connect task, the network, or a
snapshot hogging the connector. Leave the slot alone. It costs disk
until the consumer catches up; watch safe_wal_size if
you set a limit. See
Observability and
Ops: Offsets & Replays.
|
| Dead consumer, slot still healthy |
active = false; confirmed_flush_lsn does
not move between samples; wal_status is not
lost.
|
Restart the connector. It resumes from the slot, and some changes
will be delivered again, so the sink must already be idempotent
(below). If it cannot come back before the
disk fills, extend the volume. Do not lower
max_slot_wal_keep_size to “buy time”: a
limit below the slot's current restart_lag makes the
slot unreserved, and it is invalidated at the next
checkpoint, which moves you to the invalidated-slot row.
|
| Abandoned slot |
active = false for a long time and no running
connector, pipeline or product claims the slot_name.
|
Confirm ownership, then drop it (next section). Nothing is lost if it really is abandoned. If you are wrong, you have just created the last row for someone's live pipeline. |
| Invalidated slot |
wal_status = 'lost' or, on 17+,
invalidation_reason IS NOT NULL (for example
wal_removed, or idle_timeout on 18).
|
The position is gone. Stop the connector, drop the slot, discard the old offset, and re-snapshot into the idempotent sink; then reconcile. A snapshot copies the rows that exist now, so rows deleted while the slot was lost stay in a sink that you did not rebuild, until a diff finds them. See Snapshotting and Reconciliation & Offset Surgery. |
Do not restart the connector against a fresh slot while keeping the old offset. Debezium's own upgrade procedure explains why: a new slot can only return changes from after it was created, so when the connector asks for the stored offset it can skip the existing change events and resume from the newest position, silently, with no records for what it skipped. Debezium documents a manual procedure for a rebuilt slot (rename the connector, remove old offsets, recreate the slot and publication, choose a snapshot mode); follow the version of that procedure that matches your Debezium.
Dropping an abandoned slot safely
-
Identify the owner. Match
slot_name,pluginanddatabaseagainst your connector configs (slot.name), your managed ELT products and your runbooks. If you cannot find an owner, ask before dropping. -
Stop the consumer first. Dropping an active slot raises
an error by default; the replication-protocol command has a
WAIToption that waits instead. Checkactive = false. -
Write down what you are giving up. Save the row from
the triage query (
restart_lsn,confirmed_flush_lsn,wal_status). It is the evidence of how far the pipeline had got. -
Drop it.
The function drops a physical or logical slot and is the same as the replication commandSELECT pg_drop_replication_slot('my_abandoned_slot');DROP_REPLICATION_SLOT. Re-run the triage query to confirm it is gone, then watch the WAL directory size over the following checkpoints rather than expecting an instant change. - Clean up the consumer side. If the pipeline is retired, delete the connector and its offsets, drop its publication and heartbeat table, and remove it from monitoring. If the pipeline continues, you are in the invalidated-slot row: the old offset is not safe to reuse, and a re-snapshot is the way back.
Dropping is a decision about the pipeline's history, not a rule you apply at a given size. “Drop the slot when it exceeds N GB” is how a live pipeline ends up in the invalidated-slot row.
After a restart: replay, duplicates and the idempotent sink
The slot confirms what the connector received. Debezium publishes to Kafka and Kafka Connect commits offsets on its own schedule; Debezium's docs tie the two together, noting that when no offsets are committed the connector has no opportunity to send the latest LSN to the database. So there is always a window of changes that were delivered downstream but not yet confirmed to the slot. A restart, a crash (the slot may return to its last checkpointed position) or a failover sends them again. Debezium documents this as at-least-once delivery.
The fix lives in the sink, not in Postgres:
- Key the target on the primary key.
-
Carry the change's log position (for Postgres,
Debezium's
source.lsn) into the target, and apply a change only when its position is newer than the one stored for that key. A replay then does nothing, and an older change arriving after a newer one cannot overwrite it. Do not order by an event timestamp. - Keep deletes as delete markers with their own log position, so a replayed insert cannot resurrect a deleted row.
The worked patterns are on Partitioning: version guards and watermarks, Materialization 101 and Snapshotting: idempotent apply. They matter doubly after slot loss: a re-snapshot is the one time rows from two different eras meet in the sink, and a position guard is what lets the snapshot and the stream converge. The related glossary entries are idempotent write and effectively-once.
Try a re-insert in the playground
The Change Feed Playground has a Re-insert after Update template. It is not a replay: an earlier version of a row is written again as an ordinary later source write, and the sink applies it in order. No old event is delivered a second time, so it cannot show a stale change losing to a newer one; for that, see the guarded-apply examples above. Pick it from the template list to see how each capture method reports that write. The playground is a simulation with made-up data; it has no Postgres slots.
Alerting: policy, not magic numbers
Any threshold quoted without the database's write rate and the volume's free space is a guess. Write the alerts as rules, then derive the numbers from your own measurements. To measure the write rate, take the WAL position twice and difference it:
-- psql: remember the position now ...
SELECT pg_current_wal_lsn() AS t0 \gset
-- ... and later, bytes of WAL written since then
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), :'t0')) AS written_since_t0;
| Alert when | Derive the threshold from |
|---|---|
| A slot has had no active consumer for longer than the pipeline can tolerate |
That pipeline's recovery objective: time to notice, restart and
catch up. Use inactive_since on 17+, or two samples of
active on older versions.
|
| Estimated time-to-full for the WAL volume is shorter than on-call response time | Free space divided by the measured WAL growth rate, including the WAL a stalled slot is pinning. This is the one that keeps the primary up. |
wal_status leaves reserved; page on unreserved, treat lost as an incident |
Your max_slot_wal_keep_size and your WAL rate.
safe_wal_size is the countdown; alert at a fraction of
the limit chosen so someone can act before it reaches zero.
|
confirmed_flush_lsn has not moved over a window while pg_current_wal_lsn() has |
A window several times longer than your heartbeat interval (or, if you run no heartbeats, longer than your normal quiet period). It catches both a stuck consumer and a quiet table with no heartbeat. |
| Heartbeat records stop arriving at the sink | The heartbeat interval, if you use heartbeats. It tells you the whole path is alive, not only the connector. |
age(catalog_xmin) keeps rising on a slot |
The same guardrails you already use for transaction ID wraparound on this cluster. |
Wire these into the dashboards and routes described on Observability Basics, and link each alert to the matching row of the decision table above.
What was checked, and what was not
-
Read in the primary docs on 2026-10-09: the
PostgreSQL pages for
pg_replication_slots(14, 16, 17, 18), replication configuration (13, 16, 17, 18), logical decoding concepts (17), the replication protocol (17), the administration functions (17), logical replication failover (17) and the PostgreSQL 17 release notes, plus the Debezium PostgreSQL connector reference (stable). -
Run: the monitoring queries were executed on PGlite
0.5.8, which embeds PostgreSQL 18.3, with
wal_level = logicaland a realpgoutputslot that nothing consumed. They parse, plan and return the columns shown. On that instance, withmax_slot_wal_keep_size = 32MB, writing about 50 MB of WAL took the slot fromreserved(safe_wal_size41 MB) tounreserved(safe_wal_sizehad gone negative, -10048 kB), and aCHECKPOINTthen moved it tolostwith NULLrestart_lsnandsafe_wal_sizeandinvalidation_reason = 'wal_removed'. Peeking at the lost slot failed with “can no longer access replication slot”, andpg_drop_replication_slotremoved it. Withidle_replication_slot_timeout = 1san unused slot becamelostwithinvalidation_reason = 'idle_timeout'after aCHECKPOINT, and itsrestart_lsnstayed set; the triage query shows a NULLrestart_lagfor it. The heartbeat table DDL, the action query andALTER PUBLICATION ... ADD TABLEalso ran. -
Not run: an actively streaming consumer. Decoding
pending changes in PGlite (
pg_logical_slot_peek_binary_changeswithpgoutput) returned nothing and the instance then stopped returning rows, soactiveandactive_pidwere only checked as columns and never seen astrue; PostgreSQL 14 to 17 behaviour (docs only), failover slots and synchronisation, the Debezium JSON, whether Debezium'sfilteredmode really removes a manually added heartbeat table, and any managed service (RDS, Cloud SQL) or the Fivetran product. Settings that depend on your Debezium release (lsn.flush.mode,slot.failover) are marked “check your version” for that reason.
Replication Slots Knowledge Check
Four questions on what a slot holds, what caps it, and what recovery owes the sink.
A connector has been stopped for two days. What does the primary do with the WAL written meanwhile?
A slot persists across crashes and knows nothing about its consumer. The server keeps every WAL file from restart_lsn onward until the slot moves forward, is dropped, or is invalidated by max_slot_wal_keep_size. A publication filters what is sent, not how much WAL is kept, and wal_keep_size is a separate setting for physical standbys.
Review the correct answer and explanation.
Which setting caps how much WAL replication slots are allowed to retain?
max_slot_wal_keep_size is the cap for slots. wal_keep_size is a minimum kept for standbys, max_wal_size is a soft limit that drives checkpoints, and heartbeat.interval.ms is a Debezium setting that helps the connector confirm positions, not a Postgres limit.
Review the correct answer and explanation.
A slot shows wal_status = 'lost'. What is the safe recovery?
A lost slot cannot be used again, and the WAL it needed is gone. A new slot only sees changes from its own creation point, so keeping the old offset can skip changes silently. Re-snapshot into a sink that is keyed by primary key and ordered by log position, then reconcile, because a snapshot cannot tell you which rows were deleted while the slot was lost.
Review the correct answer and explanation.
After a restart, the same change reaches the sink twice. What makes that harmless?
The slot confirms what the connector received, not what the sink applied, and a slot's position can move back after a crash. Delivery is at-least-once, so the sink has to be idempotent: key by primary key, order by log position, never by a timestamp.
Review the correct answer and explanation.
Further resources
On this site
- Ops: Offsets & Replays for stored offsets and safe rewinds.
- Snapshotting for the re-snapshot after a lost slot.
- Backfill and Re-Snapshot Safely for choosing a reload and sweeping the deletes a snapshot cannot carry.
- Reconciliation & Offset Surgery for repairing the sink afterwards.
- Connector Config Builder for generating a Postgres connector config with a distinct slot and publication.
- Postgres quickstart and Troubleshooting.
Primary sources (read 2026-10-09)
- PostgreSQL: pg_replication_slots (columns,
wal_statusvalues,safe_wal_size; 14 and 17 differ in the newer columns). - PostgreSQL: replication configuration (
max_slot_wal_keep_size,wal_keep_size,idle_replication_slot_timeout,synchronized_standby_slots,sync_replication_slots). - PostgreSQL: logical decoding concepts (replication slots, slot synchronisation and the disk-usage caution).
- PostgreSQL: logical replication failover (the
failover_readycheck). - PostgreSQL: streaming replication protocol (
START_REPLICATION,DROP_REPLICATION_SLOT, standby status updates). - PostgreSQL: system administration functions (
pg_drop_replication_slot,pg_wal_lsn_diff,pg_ls_waldir). - Debezium connector for PostgreSQL (WAL disk space consumption, heartbeats,
slot.*and snapshot settings, failure handling, topologies).