pg_create_logical_replication_slot step-by-step

pgcreatelogicalreplicationslot is the single function call that provisions the durable WAL cursor a Change Data Capture (CDC) pipeline resumes from, and this…

pg_create_logical_replication_slot is the single function call that provisions the durable WAL cursor a Change Data Capture (CDC) pipeline resumes from, and this page isolates its exact argument matrix, reserved-state verification, and safe rollout as the creation step within initializing replication slots. Get the arguments wrong and the failure is deferred, not avoided: a mis-chosen output plugin breaks the consumer’s wire decode, a temporary slot silently vanishes on reconnect and forces a full re-snapshot, and an unconsumed slot pins restart_lsn until pg_wal fills the disk and the primary refuses commits.

Everything below assumes you are already operating a production primary on PostgreSQL 14 through 17, have defined the exposure boundary in a publication, and need this slot to survive consumer restarts, WAL churn, and eventual failover. Before the function will usefully retain and decode WAL, the server-level decoding surface must already exist — these are static parameters and changing wal_level requires a full restart, so provision headroom before the first slot is needed, not during an incident:

sql
-- Publisher prerequisites. wal_level and the slot/sender ceilings each need a restart.
ALTER SYSTEM SET wal_level              = 'logical';
ALTER SYSTEM SET max_replication_slots  = 20;      -- >= total logical + physical slots
ALTER SYSTEM SET max_wal_senders        = 24;      -- >= max_replication_slots + background overhead
ALTER SYSTEM SET max_slot_wal_keep_size = '20GB';  -- PG 13+: cap WAL a stalled slot can pin
-- Restart the cluster, then confirm:
SHOW wal_level;               -- must return: logical
SHOW max_replication_slots;   -- must be > concurrent CDC consumer count
pg_create_logical_replication_slot arguments and the durable slot lifecycle The single call SELECT pg_create_logical_replication_slot() provisions the durable WAL cursor and returns (slot_name, lsn), where lsn is the slot's initial restart_lsn. Its five arguments each fix one property. slot_name (e.g. 'cdc_etl_pipeline_v1') becomes the unique row in pg_replication_slots — the durable resume key, database-scoped so it decodes only the connected database. plugin ('pgoutput') sets the wire format the consumer decodes, feeding native subscriptions, Debezium, or a custom parser. temporary=false keeps the row alive after the session ends; true drops the slot on disconnect and forces a full re-snapshot. two_phase=false decodes at COMMIT; true (PG 14+) decodes PREPARE and COMMIT PREPARED. failover=false keeps the slot on the primary only; true (PG 17+) lets pg_sync_replication_slots copy it to a standby so it survives promotion. Below, the durable slot walks a lifecycle: RESERVED (restart_lsn anchored, confirmed_flush_lsn NULL) transitions on consumer connect to ACTIVE (consumer attached, confirmed_flush_lsn advances), on disconnect to INACTIVE (WAL still pinned because the durable row persists), and if WAL retention exceeds max_slot_wal_keep_size to INVALIDATED (wal_status = lost, re-snapshot required). An inactive slot can reconnect and resume from confirmed_flush_lsn. pg_create_logical_replication_slot — arguments and the durable slot lifecycle THE CALL ARGUMENT PROPERTY IT FIXES pg_create_logical_ replication_slot() one call provisions the durable WAL cursor returns (slot_name, lsn) → initial restart_lsn slot_name 'cdc_etl_pipeline_v1' Unique row in pg_replication_slots — the durable resume key database-scoped: decodes only the connected database plugin 'pgoutput' Wire format the consumer decodes — fixed at creation pgoutput → native subscriptions · Debezium · custom parser temporary false (durable) Durable row survives session end true drops the slot on disconnect → forces a full re-snapshot two_phase false → COMMIT Decodes at COMMIT (default) true (PG 14+): decode PREPARE / COMMIT PREPARED · fixed at creation failover false (PG 17+) Slot lives on the primary only true (PG 17+): pg_sync_replication_slots → survives promotion DURABLE SLOT LIFECYCLE · temporary = false RESERVED restart_lsn anchored confirmed_flush_lsn = NULL wal_status = reserved ACTIVE consumer attached confirmed_flush_lsn advances active = true INACTIVE disconnected — WAL pinned durable row persists active = false INVALIDATED past max_slot_wal_keep_size wal_status = lost re-snapshot required connects disconnects WAL cap hit reconnect → resume from confirmed_flush_lsn

Argument Semantics and Slot Behavior

The function signature is pg_create_logical_replication_slot(slot_name name, plugin name [, temporary boolean, two_phase boolean, failover boolean]) and it returns a single (slot_name, lsn) row, where lsn is the WAL position captured as the slot’s initial restart_lsn. Each argument fixes a different durability or decode property; the table maps every one to the guarantee it buys and the logical-replication behavior it changes.

Argument Value Behavior guarantee Logical-replication effect
slot_name e.g. cdc_etl_pipeline_v1 Unique per cluster; the durable key the consumer resumes against Slots are database-scoped — the slot only decodes changes in the database you were connected to at creation. Cross-database decoding is unsupported.
plugin pgoutput (default for native replication) Binary wire format, publication-filtered, lowest CPU The PostgreSQL 10+ native output plugin. Streams changes for pgoutput consumers (native subscriptions, Debezium connector, or a custom pgoutput parser). Choose test_decoding only for diagnostics, never production.
temporary false (default) Durable row that survives session end true drops the slot the moment its creating session disconnects — the cursor and all offset tracking are destroyed, forcing a full re-snapshot on the next consumer start. Durable CDC always uses false.
two_phase false (default) Decodes at COMMIT, not PREPARE true (PG 14+) decodes PREPARE TRANSACTION before the final commit, required only when the consumer must observe two-phase-commit boundaries. Cannot be toggled after creation; recreate the slot to change it.
failover false (default) Slot stays on the primary only true (PG 17+) lets pg_sync_replication_slots() copy the slot to physical standbys so it survives standby promotion. Absent on PG 16 and earlier — attempting to pass it raises a function-signature error.

Two argument interactions cause the most production incidents. First, plugin is fixed at creation: switching a live pipeline from pgoutput to another decoder means dropping and recreating the slot, which resets the cursor and mandates a re-snapshot — treat the plugin choice as permanent. Second, the executing role must hold the REPLICATION attribute and be connected to the target database; a role without it fails with ERROR: must be superuser or replication role to use replication slots, and connecting to postgres instead of the CDC source database silently creates a slot that decodes the wrong database’s WAL.

sql
-- Grant the minimum privileges before the first call.
ALTER ROLE cdc_pipeline_user REPLICATION;
GRANT CONNECT ON DATABASE target_db TO cdc_pipeline_user;

The canonical persistent creation call for a native CDC pipeline:

sql
-- PG 14-17. Connect directly to the CDC source database first.
SELECT pg_create_logical_replication_slot(
    slot_name := 'cdc_etl_pipeline_v1',
    plugin    := 'pgoutput',
    temporary := false,
    two_phase := false
);

-- PG 17+ only: add failover so the slot syncs to a physical standby.
SELECT pg_create_logical_replication_slot(
    slot_name := 'cdc_etl_pipeline_v1',
    plugin    := 'pgoutput',
    temporary := false,
    two_phase := false,
    failover  := true
);

Diagnostic Patterns

Immediately after creation, verify the slot entered the reserved state with its LSNs anchored where you expect. A freshly created slot that has never had a consumer connect has a non-null restart_lsn (WAL is already being retained from here) but a null confirmed_flush_lsn — that null is normal at creation and only advances once a consumer acknowledges processed changes.

sql
-- Confirm reserved state and initial LSN anchoring right after creation.
SELECT slot_name, plugin, slot_type, active,
       restart_lsn, confirmed_flush_lsn, wal_status
FROM pg_replication_slots
WHERE slot_name = 'cdc_etl_pipeline_v1';

Interpret the state fields against concrete thresholds:

  • confirmed_flush_lsn IS NULL for > 300 s after a consumer was supposed to attach — the consumer never connected or never sent feedback. WAL is retained from restart_lsn with nothing draining it.
  • wal_status = 'extended' — the slot is retaining more WAL than max_slot_wal_keep_size nominally allows and will be invalidated if the consumer does not advance. Intervene immediately.
  • wal_status = 'lost' — WAL the slot needed has already been removed; the slot is invalid and the consumer must re-snapshot.
  • active = false for > 300 s on a temporary = false slot — the consumer disconnected but the slot (correctly) persists, so WAL retention continues to grow. Reconnect or drop deliberately.

To separate a healthy-but-idle slot from a slot silently accumulating WAL, measure the retention gap between the current WAL position and restart_lsn:

sql
-- Retention pressure: how much WAL this slot is pinning right now.
SELECT slot_name,
       active,
       wal_status,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS pending_retention
FROM pg_replication_slots
WHERE slot_name = 'cdc_etl_pipeline_v1';

Alert when pending_retention exceeds 5 GB (consumer-lag investigation) and page on wal_status = 'extended'. The reusable exporter and alert rules for these metrics belong in asynchronous monitoring integration rather than ad-hoc polling; the LSN arithmetic beneath every threshold here is covered in WAL stream mechanics.

Safe Deployment Sequence

Slot creation is not idempotent — a second call with an existing name raises ERROR: replication slot "cdc_etl_pipeline_v1" already exists — so provisioning must be guarded, verified, and reversible. Roll it out in five steps with the revert one command away.

1. Verify prerequisites and privileges. Confirm SHOW wal_level; returns logical, that free slot headroom exists (SELECT count(*) FROM pg_replication_slots; well under max_replication_slots), and that the executing role has REPLICATION. Do this before the change window, not inside it.

2. Create the slot idempotently. Wrap the call so a re-run converges instead of erroring — essential when the same deploy script runs across environments or retries:

sql
DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM pg_replication_slots WHERE slot_name = 'cdc_etl_pipeline_v1'
  ) THEN
    PERFORM pg_create_logical_replication_slot('cdc_etl_pipeline_v1', 'pgoutput');
  END IF;
END $$;

3. Validate reserved state. Run the first diagnostic query above and confirm slot_type = 'logical', a non-null restart_lsn, and wal_status = 'reserved'. Do not attach a consumer until this passes — an unverified slot with a wrong plugin only fails once the consumer tries to decode.

4. Attach the consumer and confirm the cursor moves. Start the CDC consumer, then re-query and confirm active = true and confirmed_flush_lsn has advanced past its initial null. A slot that stays at null under load means the consumer is not sending feedback — treat it as a failed rollout.

5. Keep the revert ready. Dropping the slot is the revert, and it is destructive: it discards restart_lsn so the consumer can no longer resume without a fresh snapshot. Never drop a slot to “free disk” during an incident — prefer letting max_slot_wal_keep_size invalidate it deliberately. Only run this once you accept a re-snapshot:

sql
SELECT pg_drop_replication_slot('cdc_etl_pipeline_v1');   -- destroys the cursor; re-snapshot required

Pipeline Integration

A native subscription consumes the slot for you, but Python ETL consumers must speak the logical replication protocol directly and, critically, advance the confirmed LSN themselves. Using psycopg2’s LogicalReplicationConnection, the consumer sends periodic standby status update messages with the last processed LSN; if it never sends these acknowledgments, confirmed_flush_lsn stalls and the slot bloats WAL exactly as an idle slot would.

python
# Resume-from-last-ack consumer with retry/backoff. Safe against reconnects.
import time, random, psycopg2
from psycopg2.extras import LogicalReplicationConnection

DSN = "host=primary-db dbname=cdc_source user=replicator replication=database"

def stream_changes():
    backoff = 0.5
    while True:
        try:
            conn = psycopg2.connect(DSN, connection_factory=LogicalReplicationConnection)
            cur = conn.cursor()
            cur.start_replication(
                slot_name='cdc_etl_pipeline_v1',
                options={'proto_version': '1', 'publication_names': 'cdc_pub'},
                decode=False,
            )

            def on_change(msg):
                apply_idempotent(msg.payload)              # INSERT ... ON CONFLICT DO UPDATE
                cur.send_feedback(flush_lsn=msg.data_start)  # advances confirmed_flush_lsn

            cur.consume_stream(on_change)
            backoff = 0.5
        except psycopg2.errors.ObjectInUse:
            # active = true elsewhere; only one consumer may hold a slot.
            time.sleep(backoff); backoff = min(backoff * 2, 30)
        except psycopg2.OperationalError:
            time.sleep(backoff + random.random() * 0.25)   # jitter avoids thundering herd
            backoff = min(backoff * 2, 30)

Three integration rules are specific to a freshly created slot:

  • Idempotent apply. Because a consumer can reconnect and replay from the last acknowledged LSN, every change must converge on replay — key upserts on the deterministic primary key (ON CONFLICT DO UPDATE, or PG 15+ MERGE), never blind inserts.
  • Reconcile, don’t recreate, after a stall. If the consumer processed changes but crashed before acknowledging, use SELECT pg_replication_slot_advance('cdc_etl_pipeline_v1', '<lsn>'); to move the cursor forward rather than dropping and recreating the slot, which would force a full re-snapshot.
  • Failover handling. Before PostgreSQL 17, logical slots do not migrate on primary failover: the promoted standby has no slot, and the consumer’s ERROR: replication slot "cdc_etl_pipeline_v1" does not exist is the trigger to recreate the slot on the new primary and drive a snapshot-based catch-up via subscription sync procedures. On PG 17+, a slot created with failover := true and synced through pg_sync_replication_slots() survives promotion — verify it exists on the standby before you promote, because a slot that was never synced is gone the moment the old primary is lost.

Authoritative references