Estuary

PostgreSQL Replication Slots: How to Monitor, Recover, and Prevent WAL Bloat

Replication slots protect CDC continuity, but stalled consumers can retain excessive WAL or make slots unusable.

Picture of Dani Pálma
Dani Pálma Developer Relations Lead
Picture of Emily Lucek
Emily Lucek Developer Advocate / Data Engineer
PostgreSQL replication slots
Share this article

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:

FieldWhat it tells youOperational meaning
activeWhether the slot is currently being streamedUseful connection signal, but not proof that data is progressing
restart_lsnOldest WAL location the slot might still requireThe WAL-retention floor; a stuck value can drive disk growth
confirmed_flush_lsnLatest location acknowledged by the logical consumerThe consumer's confirmed progress
wal_statusWhether required WAL is reserved, extended, unreserved, or lostlost means the slot is no longer usable
safe_wal_sizeAdditional WAL bytes that can be written before the slot risks becoming lostPopulated when a finite WAL-retention limit applies; NULL when retention is unlimited or the slot is already lost
invalidation_reasonWhy a slot was invalidatedAvailable 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:

  1. 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.
  2. 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:

sql
SELECT 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:

ObservationLikely meaningNext check
Both LSNs advance and retained WAL stays boundedSlot is probably healthyConfirm end-to-end destination freshness
confirmed_flush_lsn is staticConsumer is not acknowledgingConnector health, downstream backpressure, and network state
confirmed_flush_lsn advances but restart_lsn does notPostgreSQL still needs older WALLong-running transactions and large transactions
active = false and retained WAL growsConsumer is disconnected or slot is orphanedWhether the connector should still exist
active = true but LSNs do not moveConnection exists without useful progressConsumer logs, wait events, and a stale network session
wal_status = 'unreserved'Required WAL is at risk of removalRestore progress immediately and check the configured cap
wal_status = 'lost'Required WAL has been removedRecreate 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:

sql
SELECT 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 stateSafest actionWhy
Slot usable; required WAL presentFix the consumer and resume the existing slotPreserves the original change stream
Slot lost or missing; writes were provably paused after the consumer caught upCreate a new slot and start from the new current positionThere is no unobserved change window
Slot lost or missing; writes continuedRecreate the slot and run a connector-supported full or incremental recoveryChanges may be missing from the stream
Deletes may have occurred during the gapUse a recovery that reconciles the full current source stateRow scans limited to inserts and updates cannot infer missing rows
Recovery history or last safe position is unknownPrefer a full consistency rebuild or comparisonGuessing 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:

sql
SELECT 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:

  1. Find the last transaction ID recorded by the capture before failure.
  2. Set Minimum Backfill XID in the capture's advanced configuration.
  3. Trigger an Incremental Backfill (Advanced) for all affected bindings.
  4. Monitor slot creation, table backfills, and resumed replication.
  5. 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

    What is the difference between restart_lsn and confirmed_flush_lsn?

    confirmed_flush_lsn is the latest WAL location a logical consumer has acknowledged. restart_lsn is the oldest WAL location PostgreSQL may still need to retain for the slot. A long-running transaction can hold restart_lsn back even while confirmed_flush_lsn advances.
    The slot is retaining WAL because its restart_lsn is far behind the current WAL position. Common causes are a stopped or lagging consumer, an orphaned slot, or a long-running transaction. Measure the difference with pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) and investigate the trend.
    It means PostgreSQL has removed WAL that the replication slot still required. The slot is no longer usable. You must establish a new slot and use your replication or CDC system's rebootstrap procedure to restore consistency.
    Sometimes. If writes were paused after the consumer caught up, a new slot can start at the current position without missing changes. Some CDC systems also support a transaction-ID-limited incremental backfill. If writes continued and deletes may have occurred, a full-state reconciliation is safer.
    The safe value is workload-specific. Multiply peak WAL generation by the longest consumer outage you plan to tolerate, then add headroom for long transactions and operational response time. Keep the result below a disk-usage threshold that protects the database itself.

Start streaming your data for free

Build a Pipeline

About the authors

Picture of Dani Pálma
Dani PálmaDeveloper Relations Lead

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.

Picture of Emily Lucek
Emily LucekDeveloper Advocate / Data Engineer

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.

Streaming Pipelines.
Simple to Deploy.
Simply Priced.
$0.50/GB of data moved + $.14/connector/hour;
50% less than competing ETL/ELT solutions;
<100ms latency on streaming sinks/sources.