
A PostgreSQL replication slot prevents the database from removing Write-Ahead Log (WAL) records that a replication consumer may still need. That protects change data capture (CDC) continuity, but it also creates an operational risk: if the consumer stalls, the slot can retain WAL until the disk fills. If WAL retention is capped, PostgreSQL may instead remove required WAL and make the slot unusable.
The practical rule is simple: monitor both the consumer's confirmed position and the slot's oldest required WAL position. If a slot is lost or dropped, do not assume that recreating it preserves continuity. First determine whether writes occurred during the gap, then choose a recovery method that can reconcile the missing inserts, updates, and deletes.
Key Takeaways
confirmed_flush_lsn shows how far a logical consumer has acknowledged the stream.
restart_lsn shows the oldest WAL position PostgreSQL may still need for that slot.
A stalled consumer can stop both positions; a long-running transaction can hold back restart_lsn even while acknowledgements continue.
max_slot_wal_keep_size = -1 permits unlimited slot-driven WAL retention. A finite limit protects disk capacity but can cause a lagging slot to become lost.
If wal_status = 'lost', the old slot cannot continue. Recovery requires a new slot plus a connector-specific snapshot, backfill, or rebootstrap strategy.
An XMIN-limited backfill can recover inserts and updates in some CDC systems, including Estuary, but it does not discover rows deleted during the gap.
What is a PostgreSQL replication slot?
A PostgreSQL replication slot is a persistent, cluster-wide object that records what a replication consumer still needs. Physical slots retain WAL for physical replicas. Logical slots support logical decoding and CDC consumers that transform WAL records into row-level change events.
Unlike a normal client connection, a slot remains after the consumer disconnects. That persistence is the feature: a connector can reconnect and continue from its previous position. It is also the hazard because PostgreSQL may continue retaining WAL for an inactive or abandoned slot.
For a logical replication slot, these fields in pg_replication_slots matter most:
| Field | What it tells you | Operational meaning |
|---|---|---|
| active | Whether the slot is currently being streamed | Useful connection signal, but not proof that data is progressing |
| restart_lsn | Oldest WAL location the slot might still require | The WAL-retention floor; a stuck value can drive disk growth |
| confirmed_flush_lsn | Latest location acknowledged by the logical consumer | The consumer's confirmed progress |
| wal_status | Whether required WAL is reserved, extended, unreserved, or lost | lost means the slot is no longer usable |
| safe_wal_size | Additional WAL bytes that can be written before the slot risks becoming lost | Populated when a finite WAL-retention limit applies; NULL when retention is unlimited or the slot is already lost |
| invalidation_reason | Why a slot was invalidated | Available in newer PostgreSQL versions; documented values are wal_removed, rows_removed, wal_level_insufficient, and idle_timeout |
confirmed_flush_lsn and restart_lsn answer different questions. The consumer can acknowledge committed transactions beyond an older open transaction, so confirmed_flush_lsn may advance while restart_lsn remains pinned. Monitoring only one of them can hide a production risk.
How do replication slots cause WAL bloat?
PostgreSQL normally recycles WAL after it is no longer needed for recovery, archiving, or replication. A slot moves that cleanup boundary backward to its restart_lsn. The retained amount is approximately: current WAL LSN - restart_lsn
Two patterns commonly create WAL growth:
- The consumer stops acknowledging changes. A crashed connector, downstream backpressure, a network failure, or an orphaned slot can prevent confirmed_flush_lsn from advancing. The slot continues retaining WAL.
- An old transaction delays logical decoding cleanup. Logical decoding must preserve transaction boundaries. A long-running transaction that has generated changes can keep restart_lsn behind even when the consumer is otherwise healthy.
WAL is generated across the PostgreSQL cluster, not only by the tables in one publication. A low-volume captured table can therefore be associated with substantial retained WAL when the rest of the cluster is write-heavy.
Run this PostgreSQL replication slot health check
The following query shows slot activity, retained WAL, and acknowledgement lag for logical slots:
sqlSELECT
slot_name,
database,
active,
active_pid,
restart_lsn,
confirmed_flush_lsn,
wal_status,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)::bigint
) AS retained_wal,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)::bigint
) AS acknowledgement_lag,
CASE
WHEN safe_wal_size IS NULL THEN NULL
ELSE pg_size_pretty(safe_wal_size)
END AS safe_wal_remaining
FROM pg_replication_slots
WHERE slot_type = 'logical'
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC NULLS LAST;Interpret the result as a trend, not a one-time snapshot:
| Observation | Likely meaning | Next check |
|---|---|---|
| Both LSNs advance and retained WAL stays bounded | Slot is probably healthy | Confirm end-to-end destination freshness |
| confirmed_flush_lsn is static | Consumer is not acknowledging | Connector health, downstream backpressure, and network state |
| confirmed_flush_lsn advances but restart_lsn does not | PostgreSQL still needs older WAL | Long-running transactions and large transactions |
| active = false and retained WAL grows | Consumer is disconnected or slot is orphaned | Whether the connector should still exist |
| active = true but LSNs do not move | Connection exists without useful progress | Consumer logs, wait events, and a stale network session |
| wal_status = 'unreserved' | Required WAL is at risk of removal | Restore progress immediately and check the configured cap |
| wal_status = 'lost' | Required WAL has been removed | Recreate and rebootstrap; the slot cannot resume |
On PostgreSQL versions that expose invalidation_reason, add it to the query. inactive_since is available from PostgreSQL 17 onward and is worth monitoring on its own; on PostgreSQL 18 it also drives idle_replication_slot_timeout, which invalidates a slot left inactive for too long.
Find long-running and idle transactions
Use pg_stat_activity to locate transactions that deserve investigation:
sqlSELECT
pid,
usename,
application_name,
client_addr,
state,
xact_start,
now() - xact_start AS transaction_age,
wait_event_type,
wait_event,
left(query, 250) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
AND now() - xact_start > interval '10 minutes'
ORDER BY xact_start;Prioritize transactions that have made changes, unexpectedly old idle in transaction sessions, and application jobs with unusually large transaction boundaries. Do not terminate a backend based only on its age. Identify its owner and workload first; pg_terminate_backend() rolls back the transaction and can affect production traffic.
Why PostgreSQL replication slots fail in production
1. The CDC consumer stops or falls behind
If the connector cannot read and acknowledge changes, confirmed_flush_lsn stops moving. Common causes include:
- a failed or disabled capture task;
- destination throttling or backpressure;
- an expired credential or changed network policy;
- a dead tunnel or connection that still appears open;
- a connector repeatedly retrying the same transaction; or
- an abandoned slot left behind after a pipeline was deleted.
Recovery: Fix the consumer or downstream blocker first. If wal_status is still reserved or extended and the required WAL remains available, the existing slot can usually continue. Dropping a healthy slot would discard the continuity it is protecting.
2. A long-running transaction holds back restart_lsn
Logical decoding emits changes in transaction order and must retain information required to finish open transactions. A transaction that remains open while the cluster generates heavy WAL can therefore create a large gap between restart_lsn and confirmed_flush_lsn.
Large migrations, bulk updates, and backfill implementations with oversized transaction boundaries are frequent triggers. An idle transaction should also be investigated, particularly if it has performed writes.
Recovery: Commit or roll back the responsible transaction through the owning application, then verify that restart_lsn advances. For future jobs, use smaller transaction boundaries and monitor transaction age. Connector backfill chunk size is product-specific; avoid presenting one universal row count as safe for every schema and workload.
3. Required WAL exceeds max_slot_wal_keep_size
max_slot_wal_keep_size limits how much WAL a replication slot can force PostgreSQL to retain at checkpoint time. Its default is -1, which allows unlimited retention.
- Unlimited retention favors recoverability but can allow a stalled slot to consume the disk.
- A finite limit protects storage, but a consumer that falls too far behind can lose required WAL and become unable to continue.
When the limit is approached, safe_wal_size trends toward zero. The slot can move through unreserved and eventually lost; checkpoints affect when removal occurs, so the configured value is not a precise byte-for-byte circuit breaker.
Recovery: If the slot is already lost, increasing the limit does not restore removed WAL. Fix the underlying lag, recreate the slot, and run the appropriate snapshot or backfill recovery. If the slot is not yet lost, restoring consumer progress may allow it to recover before required files are removed.
4. A major upgrade, migration, or failover changes the WAL timeline
A connector may retain an LSN from the old server while the new writer has a different timeline or lacks the old logical slot. Simply pointing the connector at the new host does not prove that it can resume safely.
Newer PostgreSQL releases provide safer native options in some topologies:
- pg_upgrade can preserve logical slots when the old cluster is already on PostgreSQL 17 or later and the documented prerequisites are met. Upgrading from PostgreSQL 16 or earlier does not preserve slots, because the source cluster predates the feature.
- Failover-enabled logical slots can be synchronized to standbys. Before promotion, verify that the target slot is synced, not temporary, and has no invalidation reason. PostgreSQL also recommends ensuring the standby is ahead of the subscriber.
These features reduce disruption, but they require planning and verification. Managed PostgreSQL services may expose different controls or impose version-specific restrictions.
Recovery: If the slot was not preserved or synchronized, treat the event as a possible CDC gap. Rebootstrap unless you can prove that no writes occurred between the last acknowledged event and the new slot position.
5. The slot is inactive for too long
PostgreSQL 18 introduced idle_replication_slot_timeout, which can invalidate a slot that remains inactive longer than the configured duration. The default of zero disables this behavior. Invalidation occurs at a checkpoint, so it may happen later than the exact timeout value.
This setting is useful for limiting forgotten-slot risk, but it changes the recovery tradeoff: an inactive pipeline may need to be reinitialized instead of resuming after a long pause.
How to recover a broken PostgreSQL replication slot
The correct recovery depends on two facts: whether the required WAL still exists and whether the source accepted writes during the gap.
| Slot and write state | Safest action | Why |
|---|---|---|
| Slot usable; required WAL present | Fix the consumer and resume the existing slot | Preserves the original change stream |
| Slot lost or missing; writes were provably paused after the consumer caught up | Create a new slot and start from the new current position | There is no unobserved change window |
| Slot lost or missing; writes continued | Recreate the slot and run a connector-supported full or incremental recovery | Changes may be missing from the stream |
| Deletes may have occurred during the gap | Use a recovery that reconciles the full current source state | Row scans limited to inserts and updates cannot infer missing rows |
| Recovery history or last safe position is unknown | Prefer a full consistency rebuild or comparison | Guessing risks silent data loss |
Step 1: Stop the condition that caused the failure
Restore the connector, relieve downstream backpressure, end the responsible transaction, or add storage headroom. Recreating a slot before fixing the root cause often repeats the incident.
Step 2: Preserve evidence before changing the slot
Record:
- the slot row from pg_replication_slots;
- current WAL position and retained-WAL estimate;
- connector logs and its last acknowledged LSN or transaction ID;
- the time the destination stopped updating;
- whether writes or deletes occurred during that interval; and
- the source server identity and timeline before and after any migration.
Step 3: Determine whether the slot is still usable
If the slot exists and has not lost required WAL, restart or repair the consumer and observe both LSNs. If the slot is lost, required WAL is gone and no setting change can reconstruct it.
Step 4: Recreate only when continuity is already lost
Dropping a slot is irreversible and should not be the first troubleshooting step. After confirming that it is unusable, drop it only when no consumer is active:
sqlSELECT pg_drop_replication_slot('your_slot_name');Then let the CDC connector recreate the slot or create it using the connector's required output plugin and configuration. A new slot starts a new continuity boundary; it does not fill the missing interval by itself.
Step 5: Reconcile the gap
Use the recovery mode supported by your CDC system:
- Full snapshot or full incremental backfill: safest when transaction history is unavailable or deletes must be reconciled.
- Transaction-ID- or XMIN-limited backfill: potentially much faster for large tables, but connector-specific and generally limited to rows that still exist.
- Skip existing rows / changes-only restart: safe only when writes were paused and the old consumer was confirmed caught up before the slot changed.
Finally, validate source and destination row counts, key business aggregates, recent inserts and updates, and deletion handling. A running connector proves liveness, not correctness.
Estuary recovery: use an XMIN-limited backfill carefully
Estuary's PostgreSQL connector can limit a recovery backfill using the last successfully processed transaction ID. This is an Estuary feature, not a generic PostgreSQL slot operation.
The current Estuary replication slot recovery guide uses this process:
- Find the last transaction ID recorded by the capture before failure.
- Set Minimum Backfill XID in the capture's advanced configuration.
- Trigger an Incremental Backfill (Advanced) for all affected bindings.
- Monitor slot creation, table backfills, and resumed replication.
- Clear Minimum Backfill XID after recovery so a future backfill is not accidentally restricted by an old cutoff.
An XMIN-limited scan can recover rows inserted or updated after the cutoff, but it cannot find a row that was deleted during the gap because that row no longer exists to be scanned. If missed deletes matter, use a full incremental backfill or another full-state reconciliation method. Also account for PostgreSQL transaction ID wraparound when choosing an old XID.
For planned maintenance, the safest no-rescan sequence is to pause writes, wait until the capture is fully current, perform the operation, reset the capture using its changes-only mode, and then resume writes. Follow the Estuary backfill documentation for the exact options and current UI behavior.
How to size max_slot_wal_keep_size
There is no universally safe value such as 10 GB or 50 GB. Base the limit on measured WAL generation and the outage window you intend to survive:
retention budget ≈ peak WAL bytes/hour × recovery window + open-transaction exposure + safety margin
For example, if the cluster produces 12 GB of WAL per hour at peak and the on-call objective is to recover a stalled consumer within four hours, the base requirement is already 48 GB. Add space for unusually large or long transactions and a safety margin, then verify that the database volume can absorb that amount without breaching its own free-space threshold.
Use measured production rates rather than averages alone. Estuary's guide to measuring PostgreSQL WAL throughput provides a SQL-based way to establish a baseline.
Alert before either limit is exhausted:
- retained WAL exceeds a percentage of the slot budget;
- safe_wal_size falls below the WAL normally generated during the response window;
- confirmed_flush_lsn has not advanced for a workload-appropriate interval;
- restart_lsn age or distance grows continuously;
- free disk space approaches the retained-WAL budget; or
- a required slot becomes inactive, unreserved, or lost.
Production prevention checklist
- Set a deliberate max_slot_wal_keep_size based on peak WAL throughput, response time, and available disk—not a copied default.
- Monitor restart_lsn, confirmed_flush_lsn, wal_status, safe_wal_size, and destination freshness outside the CDC process itself.
- Alert on trends early enough for an operator to respond before the slot becomes lost.
- Keep application transactions bounded; investigate old and idle in transaction sessions.
- Remove slots when their consumers are permanently decommissioned.
- Test connector restart, slot-loss recovery, and delete reconciliation in a non-production environment.
- For planned upgrades and failovers, document whether the slot will be migrated, synchronized, or recreated.
- Pause writes and wait for the consumer to catch up before maintenance when you intend to skip a historical backfill.
- Validate the destination after recovery; do not stop at “the task is running.”
- Route database, connector, and destination alerts to the same accountable on-call rotation.
Build PostgreSQL CDC that can recover, not just stream
A production CDC design is only as reliable as its recovery path. Replication slots provide durable progress, but they cannot protect you from unlimited WAL growth, removed WAL, a new server timeline, or a missed-delete window. Monitor the slot and destination together, size retention from measured WAL throughput, and rehearse the recovery decision before an incident.
For a broader architectural overview, read the complete guide to PostgreSQL CDC. If you want a managed PostgreSQL capture with continuous streaming and built-in backfill controls, review the Estuary PostgreSQL connector documentation or start building with Estuary.
FAQs
Why is my PostgreSQL replication slot using so much disk space?
What does wal_status = 'lost' mean?
Can I recover an invalidated replication slot without a full resnapshot?
What is a safe max_slot_wal_keep_size?

About the authors
Dani is a data professional with a rich background in data engineering and real-time data platforms. At Estuary, Daniel focuses on promoting cutting-edge streaming solutions, helping to bridge the gap between technical innovation and developer adoption. With deep expertise in cloud-native and streaming technologies, Dani has successfully supported startups and enterprises in building robust data solutions.
Emily is an engineer and technical content creator with an interest in developer education. At Estuary, she works with data pipelines for both streaming and batch data and finds satisfaction in transforming a mess of information into usable data. Previous roles familiarized her with FinTech data and working closely with REST APIs.





