Replication Topologies & Failover Operations

Running logical replication across more than one node turns a stream you configured once into a system you have to operate, and the sharpest edge is that a…

Running logical replication across more than one node turns a stream you configured once into a system you have to operate, and the sharpest edge is that a PostgreSQL logical replication slot lives only on the node that owns it — so a primary failover before PostgreSQL 17 silently discards the Change Data Capture (CDC) position and forces every downstream consumer into a full resnapshot. This reference treats the multi-node question as one operational discipline: how logical decoding behaves across single-primary CDC, cascaded relay chains, and bidirectional active-active designs, what a standby promotion does to the slots that anchor those streams, and the exact configuration, state mechanics, and diagnostics needed to keep a topology recoverable on PostgreSQL 14 through 17.

Logical replication topology and the promotion path that loses slots The primary runs with wal_level logical and owns a set of logical replication slots that feed a Debezium Kafka Connect consumer and a native subscriber. A physical standby streams WAL from the primary. When the standby is promoted, any slot that was not created with failover enabled is absent on the new primary, so its consumer must perform a full resnapshot. A separate note shows that PostgreSQL 17 synchronizes failover-enabled slots to the standby so the position survives promotion. Primary wal_level = logical Logical slots restart_lsn confirmed_flush_lsn one per consumer Debezium / Kafka Connect CDC event stream Native subscriber apply worker decode Physical standby streaming replica physical WAL stream promote Slot not on standby pre-PG 17: non-failover slots absent after promote → full resnapshot PG 17 fix failover = true + sync_replication_slots copies slots to standby
A single-primary topology and its failover hazard: logical slots live only on the primary, so a pre-PostgreSQL-17 promotion leaves the new primary with no slot and forces a resnapshot unless failover = true and slot synchronization carried the position across.

Core Architecture

A logical replication topology is defined by where the replication slots sit and where the WAL they anchor is generated. A slot is a server-local object: it exists in the pg_replslot/ directory of exactly one node, it pins that node’s WAL through restart_lsn, and it advances only when its own consumer sends feedback. Nothing about a slot is inherently portable. Physical streaming replication copies data blocks, not slot state, so a standby built with pg_basebackup and streaming replication carries a byte-identical heap but an empty set of logical slots. That asymmetry is the root of every failover problem in this reference: the data survives promotion, the CDC cursor does not.

Four topology shapes cover almost everything operators run. Single-primary CDC is one writable primary feeding one or more read-only consumers — Debezium, a native subscriber, or a Python decoder — each behind its own slot. Cascaded or relay topologies chain a subscriber that is itself a publisher, useful for fanning one source out to many regions without multiplying the decode load on the origin; the relay node owns a second generation of slots that must be recovered independently. Bidirectional / active-active designs let two nodes both write and both subscribe to each other, which introduces write conflicts and replication loops that need origin filtering to break — the subject of the bidirectional replication and conflict resolution guide. And standby promotion overlays a physical high-availability pair underneath any of the above, so the writable role can move between machines. The topologies compose: a production system is often a physical HA pair (for durability) whose current primary also runs single-primary logical CDC (for analytics).

The decoding model constrains all four shapes identically. Each slot is single-consumer and strictly ordered, decoding committed transactions in commit order through the pgoutput plugin. You get parallelism by partitioning the source into several slots and publications, never by attaching two consumers to one slot. That means a topology’s slot count is a first-class capacity and failover concern: every slot is a separate cursor that must be retained, monitored, and — on promotion — recovered or recreated. The underlying stream mechanics, including how restart_lsn and confirmed_flush_lsn move, are detailed in WAL stream mechanics; this reference is about what happens to those cursors when the node beneath them changes.

Declarative Configuration Model

A recoverable topology is described by a small set of declarative objects that must agree across nodes: the publisher’s wal_level and slots, the publications that scope the stream, the subscriptions or connectors that consume it, and — from PostgreSQL 16 onward — the failover attributes that let a slot survive promotion. Configure these as code so that a rebuilt or promoted node converges to the same shape rather than drifting.

The publisher side is the same base configuration every logical topology needs, plus the standby feedback settings that make slot synchronization possible:

sql
-- Publisher (primary). wal_level change requires a restart.
ALTER SYSTEM SET wal_level = 'logical';
ALTER SYSTEM SET max_replication_slots = '20';    -- one per consumer + headroom
ALTER SYSTEM SET max_wal_senders = '20';          -- one per slot + physical standbys
-- Bound the WAL a lagging slot can pin before it is invalidated (PG 13+).
ALTER SYSTEM SET max_slot_wal_keep_size = '20GB';

Slots created with failover semantics are the pivot of the whole topology. On PostgreSQL 17 a slot can be declared to fail over at creation time, and a native subscription can request it in one statement:

sql
-- PG 17: create a logical slot that will be synchronized to standbys.
SELECT pg_create_logical_replication_slot('cdc_orders', 'pgoutput', false, false, true);
--                                          name         plugin      temp   twophase failover

-- PG 17: a native subscription can create its slot with failover in one step.
CREATE SUBSCRIPTION sub_orders
  CONNECTION 'host=pg-primary dbname=analytics application_name=sub_orders'
  PUBLICATION pub_orders
  WITH (failover = true, copy_data = true);

The publications those slots decode are ordinary — narrow, explicitly enumerated tables with a deliberate REPLICA IDENTITY, exactly as covered in creating publications. What changes in a multi-node setting is that the same publication definition must exist on whatever node becomes primary, and any Debezium connector or native subscription must be able to point at the new endpoint without re-enumerating tables. Keep publication DDL, slot creation, and subscription definitions in the same version-controlled provisioning that builds the node, so a promoted standby is configured identically. The full mechanics of provisioning and recovering slots live in initializing replication slots, and the subscription-side resume flow in subscription sync procedures.

State Persistence & Lifecycle

The single fact that governs failover is where slot state is persisted. A logical slot’s restart_lsn, confirmed_flush_lsn, and catalog xmin are held in the owning node’s pg_replslot/<slot>/state file and checkpointed there. Physical replication never ships that file. So before PostgreSQL 16 the lifecycle of a slot across a promotion is brutally simple: the slot exists on the old primary, the standby has nothing, and promotion produces a writable node with zero logical slots. Any consumer reconnecting to the new primary finds its slot missing and — depending on the client — either errors or transparently recreates a fresh slot that begins decoding from the current WAL position, skipping everything committed between the last acknowledged LSN and the promotion. That gap is silent data loss unless the consumer resnapshots.

Slot survival across promotion by PostgreSQL version Three horizontal rows. The pre-16 row ends in a red state where the promoted node has no slot and the consumer resnapshots. The 16 row shows the standby-slot foundations that let a synchronized slot be created. The 17 row ends in a green state where the slot is present on the promoted node and the consumer resumes from confirmed_flush_lsn. On promotion PG ≤ 15 PG 16 PG 17 slot on old primary only no slot → resnapshot standby_slot_names foundation manual sync possible failover=true, slots synced resume from confirmed_flush
Slot survival across a promotion improves with each major version: silent loss and resnapshot before PostgreSQL 16, standby-slot foundations in 16, and end-to-end synchronized failover slots in 17.

PostgreSQL 16 laid the groundwork by letting a physical standby reserve WAL for downstream logical consumers via standby_slot_names, so the standby no longer recycles WAL a logical slot might still need — the precondition for a slot that can be safely resumed after promotion. PostgreSQL 17 completes the mechanism: a slot created with failover = true is copied to standbys by sync_replication_slots = on (or an explicit pg_sync_replication_slots() call), and pg_replication_slots.synced on the standby confirms the copy. After a PG 17 promotion the synchronized slot is already present on the new primary at a position at or behind the last acknowledged LSN, so the consumer reconnects and resumes with at-most a small, idempotently absorbable replay rather than a resnapshot. The exact GUCs, prerequisites, and verification queries are the whole subject of configuring failover slots in PostgreSQL 17.

Two lifecycle hazards persist regardless of version. First, a synchronized slot on a standby only stays useful if the standby is not allowed to advance past the logical consumer — hence standby_slot_names and hot-standby feedback must be configured together, or the synced slot’s restart_lsn can outrun retained WAL. Second, an over-retained slot is still a disk risk on whichever node owns it: a stuck consumer freezes restart_lsn and pins WAL until max_slot_wal_keep_size invalidates the slot, and after a failover that invalidation can happen on a node nobody is watching. The reconciliation runbook for a promoted node with a missing or stale slot is recovering logical slots after primary failover.

Security & Privilege Boundaries

A multi-node topology multiplies the surface that carries raw committed rows, so the privilege model has to hold on every node the writable role can land on, not just the one you configured first. The replication identity is effectively a read-everything credential for the captured tables; it needs REPLICATION (or, on PG 16+, the pg_create_subscription predefined role for native subscriptions) plus SELECT on the published tables, and nothing more. Because a promoted standby must accept the same connection, that role and its grants have to exist identically on both members of an HA pair — a common failover surprise is a new primary that rejects the connector because the CDC role was only ever created on the original node.

sql
-- Create the dedicated CDC role identically on every node that can become primary.
CREATE ROLE cdc_replicator WITH LOGIN REPLICATION PASSWORD 'from-secrets-manager';
GRANT USAGE ON SCHEMA public TO cdc_replicator;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO cdc_replicator;
-- PG 16+: native subscription workers can run under a non-superuser owner.
GRANT pg_create_subscription TO cdc_replicator;

Two network boundaries follow the topology. pg_hba.conf must scope the replication connection type to the CDC role from the consumer’s address on every node, so a failover does not either lock out the connector or silently widen access. And because promotion moves the endpoint, TLS must be terminated per node with a certificate valid for the shared service name the consumers connect through, not a single host — otherwise sslmode=verify-full breaks the moment the standby takes over. The host-based rules and TLS specifics are covered in security boundaries and permissions. In a bidirectional design each node is simultaneously publisher and subscriber, so the same role plays both parts on both sides; keep the credentials distinct per direction so a compromised subscriber cannot also write as the publisher.

Observability & Diagnostics

You cannot operate a failover-capable topology without a view of slot state on every node that could be primary, because the most dangerous condition — a promoted node whose slot is missing or a synchronized slot that has stopped advancing — is invisible from the old primary. Point the collector at the same service endpoint the consumers use so it always reads the current primary, and scrape pg_replication_slots for both the classic lag columns and the PG 17 failover columns. The general collector design is in async monitoring integration; the topology-specific query adds failover and synced:

sql
-- Run against whichever node is currently primary. PG 17 adds failover/synced.
SELECT
  slot_name,
  active,
  failover,                                                              -- PG 17: will this slot fail over?
  synced,                                                                -- PG 17 (standby): copied from primary?
  wal_status,                                                            -- reserved | extended | unreserved | lost
  pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn))         AS retained_wal,
  pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS flush_lag
FROM pg_replication_slots
WHERE slot_type = 'logical'
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;
-- ALERT: a production slot with failover = false on PG 17         -> will not survive promotion
-- ALERT: retained_wal > 50% of max_slot_wal_keep_size            -> approaching invalidation
-- ALERT: active = false on a production slot for > 60 s           -> consumer down, WAL pinning

On the physical layer, pg_stat_replication on the primary shows every standby and its sync_state, which is what tells you a designated failover target is actually streaming and eligible. Cross-check that the standby you expect to promote appears with state = 'streaming' and, for synchronized-slot safety, that it is named in standby_slot_names:

sql
-- On the primary: confirm the failover-target standby is streaming and eligible.
SELECT application_name, state, sync_state,
       pg_wal_lsn_diff(sent_lsn, replay_lsn) AS standby_backlog_bytes
FROM pg_stat_replication
WHERE application_name = ANY (string_to_array(current_setting('standby_slot_names'), ','));

The composite signal that matters most in a topology is not any single lag number but the join of three facts: which node is primary, whether every expected slot exists on it, and whether each slot is advancing. A promotion that leaves a slot behind reads as “zero rows for this slot on the new primary” — so alert on the absence of an expected slot name as loudly as on a bad lag value, exactly as a stale-green dashboard would otherwise hide it.

Resilience Patterns & Failure Modes

Promotion drops a non-failover slot. Signature: after a standby is promoted, the connector or subscriber reports its slot missing and either resnapshots or begins decoding from now, skipping committed changes. Root cause: the slot was created without failover = true, or the server predates PostgreSQL 17, so it lived only on the old primary. Remediation: on PG 17, recreate all production slots with failover enabled and confirm synced = true on the standby before the next switchover; before PG 17, treat every promotion as requiring the reconciliation runbook in recovering logical slots after primary failover and design consumers to be idempotent so a bounded replay is harmless.

Synchronized slot outruns retained WAL on the standby. Signature: PG 17 synced = true but after promotion the slot is wal_status = 'lost' or invalidated. Root cause: the standby recycled WAL the logical slot still needed because standby_slot_names was not set or hot-standby feedback was off, so the synced slot’s restart_lsn referenced WAL that no longer exists. Remediation: set standby_slot_names to include the physical standby and enable hot_standby_feedback = on, as detailed in configuring failover slots in PostgreSQL 17.

Replication loop in a bidirectional topology. Signature: a row change ping-pongs between two active-active nodes and re-applies indefinitely, or an update storm amplifies. Root cause: each node re-publishes changes it received from the other because origin filtering is not in place. Remediation: create subscriptions with origin = none (PG 16+) so a node never forwards changes that did not originate locally, and resolve genuine concurrent writes with a deterministic conflict policy — both covered in bidirectional replication and conflict resolution.

A schema change breaks the stream mid-topology. Signature: apply workers or Debezium halt on a relation whose columns no longer match the decoded change after a DDL deployment. Root cause: DDL is not replicated by logical replication, so a column added on the publisher without a coordinated subscriber change desynchronizes the two ends. Remediation: sequence additive-then-subtractive migrations and, where needed, propagate DDL through event triggers, as covered in online schema change management.

Cascaded relay loses the second-generation slot. Signature: the origin recovers cleanly but a downstream region behind a relay node is stuck. Root cause: the relay node is itself a publisher whose slots were not treated as failover-critical, so a relay-node failover lost the second cursor. Remediation: apply the same failover-slot discipline to every publishing node in the chain, and monitor relay slots with the same absence alerting as origin slots.

Conclusion

Operating logical replication across multiple nodes is, in the end, the discipline of never letting the writable role move faster than the slot state that anchors your CDC position. The data always survives a promotion; the cursor only survives it if you engineered for that. On PostgreSQL 14 and 15, plan every promotion as a slot-recovery event with idempotent consumers and a documented resnapshot fallback, because the slot simply is not there afterward. PostgreSQL 16 gives you the standby-side WAL retention that makes a resumable slot possible. PostgreSQL 17 closes the loop with failover = true, sync_replication_slots, and synced/failover visibility, turning a resnapshot-forcing failover into a bounded, idempotently absorbable replay — provided standby_slot_names and hot-standby feedback keep the synchronized slot inside retained WAL. Provision every node identically, monitor slot presence on whichever node is currently primary, and rehearse the switchover before you have to perform it under load.