logical-replication — logical decoding + apply
Logical replication has two sides you must not confuse:
- Publisher side — reads WAL, decodes it into logical changes, hands them to an output plugin (usually
pgoutput) which serializes them as protocol messages sent over a walsender.
- Subscriber side — the apply worker receives those messages, applies INSERT/UPDATE/DELETE to the local database, and tracks its progress via a replication origin.
Both live under src/backend/replication/logical/ (20 files). Neither is a simple codebase — worker.c alone is 194 KB; reorderbuffer.c is 162 KB.
The file map
Publisher (decoding) side
| File |
KB |
Role |
decode.c |
40 |
WAL-record-to-logical-change decoder. Reads XLogRecords, dispatches to reorderbuffer callbacks. |
reorderbuffer.c |
162 |
The big one. Assembles decoded changes into transaction-shaped batches, spills to disk on memory pressure, handles subtransactions, TOAST reassembly, streaming (in-progress) transactions. |
snapbuild.c |
65 |
Historic-snapshot construction — SnapBuild state machine (BUILDING → FULL_SNAPSHOT → CONSISTENT) that lets decoding see committed catalog state as of a chosen LSN. |
logical.c |
69 |
Logical-decoding context (LogicalDecodingContext), CreateInitDecodingContext (for slot creation), CreateDecodingContext (for reading). |
logicalctl.c |
21 |
Utilities for the logical-decoding control interface. |
logicalfuncs.c |
10 |
SQL-callable helpers: pg_logical_slot_get_changes, pg_replication_slot_advance etc. |
message.c |
3 |
pg_logical_emit_message — arbitrary in-WAL logical messages. |
Slot management
| File |
KB |
Role |
slotsync.c |
63 |
Slot-sync worker — replicates logical-slot state from primary to standby (PG 17+ failover-safe slots). |
sequencesync.c |
22 |
Sequence-value sync (PG 18+ sequences-in-logical-replication). |
Subscriber (apply) side
| File |
KB |
Role |
worker.c |
194 |
THE apply worker. Main loop, message dispatch, INSERT/UPDATE/DELETE handlers, error handling, snapshot management. When users say "apply worker" this is the file. |
applyparallelworker.c |
50 |
Parallel apply worker (PG 16+ streaming mode with parallelism). Coordinator process partitions incoming stream across N leaf workers. |
launcher.c |
46 |
Logical-rep launcher aux process. Starts + monitors apply workers per subscription. |
tablesync.c |
50 |
Initial-sync worker. Per-table: COPY the base data, then hand off streaming to the main apply worker at the right LSN. |
relation.c |
27 |
Subscriber-side relation cache. Maps publisher-side OIDs to subscriber-side relations via names, tracks column mapping. |
origin.c |
47 |
Replication origins — track subscriber progress + prevent circular replication. pg_replication_origin_* SQL functions live here. |
conflict.c |
20 |
Conflict detection + logging (PG 18+ conflict tracking). |
proto.c |
31 |
Logical replication protocol — encode/decode pgoutput messages. Shared between publisher (walsender via pgoutput) and subscriber (worker.c). |
syncutils.c |
8 |
Small helpers shared between apply and tablesync. |
Output-plugin default
src/backend/replication/pgoutput/pgoutput.c — the pgoutput plugin (default logical output). Implements the callbacks that logical.c invokes. Custom output plugins (like Debezium's decoderbufs, test_decoding) are contrib modules or extensions.
The decode → apply flow
[primary] insert row on table T
↓
WAL record written
↓
walsender starts a logical replication connection
↓
CreateDecodingContext + slot state
↓
decode.c reads XLogRecords sequentially
↓
reorderbuffer accumulates txn's changes; may spill to disk
↓
on COMMIT: reorderbuffer replays changes in-order, in the historic
snapshot from snapbuild
↓
output plugin (pgoutput) callback: apply_change → serialize as
'I'/'U'/'D' protocol messages
↓
walsender sends messages over the wire
↓
[subscriber apply worker (worker.c)] reads messages, dispatches
↓
apply_dispatch → apply_handle_insert / apply_handle_update / _delete
↓
subscriber-side execute the change (with conflict handling in
PG 18+)
↓
LogicalRepApplyLoop tracks progress via replorigin_session_setup
Slots — physical vs logical
Both physical and logical replication use replication slots (src/backend/replication/slot.c — sibling, not in the logical/ subdir). Difference:
- Physical slot — reserves WAL for a physical standby / walsender. No catalog_xmin. Just a
restart_lsn.
- Logical slot — reserves WAL AND catalog snapshots (via
catalog_xmin) for logical decoding. Requires more machinery — snapbuild state, output plugin, spill-file directory in pg_replslot/<name>/.
Slot mismanagement is a common failure mode: a stale logical slot pins WAL indefinitely and blocks vacuum from removing dead catalog tuples. Monitor pg_replication_slots.confirmed_flush_lsn vs pg_current_wal_lsn.
Streaming vs non-streaming apply
Non-streaming: the publisher accumulates a whole transaction in the reorderbuffer, sends everything on COMMIT. Simple, but slow for large transactions (memory pressure, spill).
Streaming (PG 14+): publisher sends transaction changes as they decode (with a Stream Start / Stream Stop bracket), subscriber accumulates into a spool file until the streamed transaction commits, then applies.
Parallel apply (PG 16+): with streaming + streaming = parallel on the subscription, the subscriber's launcher spawns leaf workers (applyparallelworker.c) that apply streamed transactions concurrently. Requires no schema-conflicting sub-transactions.
Conflict tracking (PG 18+)
conflict.c implements the four conflict types the apply worker can detect:
insert_exists — subscriber has a row with the same PK as the incoming INSERT.
update_missing — incoming UPDATE targets a row the subscriber doesn't have.
update_differ — target row exists but doesn't match the OLD tuple's values.
delete_missing — same shape for DELETE.
Each logs a row into pg_stat_subscription_conflicts (via pgstat) and can trigger the subscription's disable_on_error policy.
Common patch shapes
Add a new logical replication protocol message
Scenario exists: knowledge/scenarios/add-new-replication-message.md. Short:
- New byte-code in
proto.c for the message shape.
- Encoder call from
pgoutput.c (publisher).
- Decoder + dispatch in
worker.c (subscriber).
- Version-gate via
LOGICAL_PROTO_VERSION_* in proto.h.
Extend output-plugin callbacks
- New callback in
include/replication/output_plugin.h (e.g. stream_change_cb).
- Wire it in
logical.c's LogicalDecodingContext.* and the reorderbuffer replay path.
- Update
pgoutput.c + test_decoding (in contrib) to implement it.
- Extension output plugins may need updates or fall back gracefully via NULL-callback checks.
Add a new conflict type
- Extend
ConflictType enum in include/replication/conflict.h.
- Detection + logging in
conflict.c (ReportApplyConflict).
- Update
pg_stat_subscription_conflicts schema + counter in pgstat_subscription.c (touches pgstat-framework).
Debug slot-not-advancing
- Check
pg_replication_slots.confirmed_flush_lsn — subscriber isn't acknowledging.
- Check subscriber's
pg_stat_subscription.last_msg_receipt_time — is the apply worker running?
- Check
pg_stat_subscription.received_lsn vs pg_current_wal_lsn on publisher — is receipt lagging?
- Check
pg_replication_slots.wal_status — if lost, WAL was recycled.
Pitfalls
catalog_xmin retention is the silent killer — a logical slot that stops advancing prevents vacuum from removing dead catalog tuples. Cluster performance degrades silently over days.
- Reorderbuffer spill files —
pg_replslot/<name>/xid-* files. If a slot stalls, spill can fill the disk. Not always obvious it's the slot's fault.
snapbuild needs a clean starting point — creating a logical slot on a busy system may take a while (has to see all in-progress txns commit or abort before reaching CONSISTENT).
- Origin sessions leak on error — if apply worker crashes mid-transaction,
replorigin_session_setup state may need manual reset via pg_replication_origin_advance.
- Parallel apply requires no PROVIDED savepoints — a transaction using SAVEPOINT can't be parallelized; the leaf worker will error and the coordinator will retry serially.
decoding_context.streaming = false is default — a slot may not receive streamed messages unless the subscriber says streaming = on. Common misconfig.
- Publisher-side vs subscriber-side
_PG_init — for output plugin extensions: publisher loads it via output_plugin_options; subscriber-side extensions load differently. Don't share state.
pgoutput vs test_decoding vs custom — pgoutput speaks the wire format the subscriber expects; test_decoding is a debugging text serializer, NOT wire-compatible.
- Slot-sync races on failover —
slotsync.c is a background worker on standby that replicates slot state. If the standby is promoted mid-sync, slots may lag or be inconsistent. Use pg_sync_replication_slots() explicitly before failover.
Related corpus
- Idioms (9 covered):
apply-conflict-resolution, apply-handlers-insert-update-delete, apply-streaming-and-parallel, apply-worker-loop, apply-worker-loop-and-dispatch, logical-decoding-snapshot, output-plugin-callbacks, replication-origin-tracking, replication-slot-advance.
- Subsystem:
replication (the parent doc — covers physical + logical high-level).
- Scenario:
add-new-replication-message.
- Related idiom:
tablesync-initial-copy (COPY-based bootstrap for a new subscription).
- Related session logs:
2026-06-02-replication-synthesis.md, 2026-06-04-a8-include-replication.md.
Corpus-chain shortcut
python3 scripts/corpus-chain.py --scenario add-new-replication-message
python3 scripts/corpus-chain.py --file src/backend/replication/logical/worker.c
python3 scripts/corpus-chain.py --idiom apply-worker-loop
These surface the tight neighborhood: the 20 files under logical/, the 9 apply/decoding idioms, and the pgoutput integration point.
Boundary
Use this skill for the src/backend/replication/logical/ tree + pgoutput + apply-worker + reorderbuffer + slot management.
Don't use for:
- Physical streaming replication — walsender/walreceiver/basebackup are the parent
replication subsystem; different code paths, though they share slot infrastructure.
walsender / walreceiver — those handle both physical and logical connections; use the parent replication subsystem doc.
- Foreign data wrappers (
postgres_fdw) — that's not replication.
- BDR / pglogical / Debezium — external / contrib output plugins; they build ON logical decoding but their internals are their own.
1---2name: logical-replication3description: PostgreSQL's logical replication — publisher-side WAL decoding + subscriber-side apply — plus everything under `src/backend/replication/logical/`. Loads when the user asks about logical decoding, `pg_logical_slot_get_changes`, publications / subscriptions, output plugins (pgoutput / test_decoding), reorder buffer, historic snapshots (SnapBuild), the apply worker + parallel apply, conflict resolution (PG 18+ conflict tracking), replication origins, replication slots (physical vs logical, `slot.c` + `slotsync.c`), streaming mode for in-progress transactions, tablesync's initial COPY, or debugging apply-worker crashes / slot advancement / catalog_xmin retention. Skip when the ask is about physical streaming replication (walsender/walreceiver, `replication/basebackup*.c`, `replication/walsender.c` in isolation) — those are the sibling `replication` subsystem, mostly disjoint code paths.4---56# logical-replication — logical decoding + apply78Logical replication has **two sides** you must not confuse:910- **Publisher side** — reads WAL, decodes it into logical changes, hands them to an *output plugin* (usually `pgoutput`) which serializes them as protocol messages sent over a walsender.11- **Subscriber side** — the *apply worker* receives those messages, applies INSERT/UPDATE/DELETE to the local database, and tracks its progress via a *replication origin*.1213Both live under `src/backend/replication/logical/` (20 files). Neither is a simple codebase — `worker.c` alone is 194 KB; `reorderbuffer.c` is 162 KB.1415## The file map1617### Publisher (decoding) side1819| File | KB | Role |20|---|---:|---|21| `decode.c` | 40 | WAL-record-to-logical-change decoder. Reads XLogRecords, dispatches to reorderbuffer callbacks. |22| `reorderbuffer.c` | 162 | The **big one**. Assembles decoded changes into transaction-shaped batches, spills to disk on memory pressure, handles subtransactions, TOAST reassembly, streaming (in-progress) transactions. |23| `snapbuild.c` | 65 | Historic-snapshot construction — SnapBuild state machine (BUILDING → FULL_SNAPSHOT → CONSISTENT) that lets decoding see committed catalog state as of a chosen LSN. |24| `logical.c` | 69 | Logical-decoding context (`LogicalDecodingContext`), CreateInitDecodingContext (for slot creation), CreateDecodingContext (for reading). |25| `logicalctl.c` | 21 | Utilities for the logical-decoding control interface. |26| `logicalfuncs.c` | 10 | SQL-callable helpers: `pg_logical_slot_get_changes`, `pg_replication_slot_advance` etc. |27| `message.c` | 3 | `pg_logical_emit_message` — arbitrary in-WAL logical messages. |2829### Slot management3031| File | KB | Role |32|---|---:|---|33| `slotsync.c` | 63 | Slot-sync worker — replicates logical-slot state from primary to standby (PG 17+ failover-safe slots). |34| `sequencesync.c` | 22 | Sequence-value sync (PG 18+ sequences-in-logical-replication). |3536### Subscriber (apply) side3738| File | KB | Role |39|---|---:|---|40| `worker.c` | **194** | **THE apply worker**. Main loop, message dispatch, INSERT/UPDATE/DELETE handlers, error handling, snapshot management. When users say "apply worker" this is the file. |41| `applyparallelworker.c` | 50 | Parallel apply worker (PG 16+ streaming mode with parallelism). Coordinator process partitions incoming stream across N leaf workers. |42| `launcher.c` | 46 | Logical-rep launcher aux process. Starts + monitors apply workers per subscription. |43| `tablesync.c` | 50 | Initial-sync worker. Per-table: COPY the base data, then hand off streaming to the main apply worker at the right LSN. |44| `relation.c` | 27 | Subscriber-side relation cache. Maps publisher-side OIDs to subscriber-side relations via names, tracks column mapping. |45| `origin.c` | 47 | Replication origins — track subscriber progress + prevent circular replication. `pg_replication_origin_*` SQL functions live here. |46| `conflict.c` | 20 | Conflict detection + logging (PG 18+ conflict tracking). |47| `proto.c` | 31 | Logical replication protocol — encode/decode `pgoutput` messages. Shared between publisher (walsender via pgoutput) and subscriber (worker.c). |48| `syncutils.c` | 8 | Small helpers shared between apply and tablesync. |4950### Output-plugin default5152`src/backend/replication/pgoutput/pgoutput.c` — the pgoutput plugin (default logical output). Implements the callbacks that `logical.c` invokes. Custom output plugins (like Debezium's decoderbufs, test_decoding) are contrib modules or extensions.5354## The decode → apply flow5556```57[primary] insert row on table T58 ↓59WAL record written60 ↓61walsender starts a logical replication connection62 ↓63CreateDecodingContext + slot state64 ↓65decode.c reads XLogRecords sequentially66 ↓67reorderbuffer accumulates txn's changes; may spill to disk68 ↓69on COMMIT: reorderbuffer replays changes in-order, in the historic70snapshot from snapbuild71 ↓72output plugin (pgoutput) callback: apply_change → serialize as73'I'/'U'/'D' protocol messages74 ↓75walsender sends messages over the wire76 ↓77[subscriber apply worker (worker.c)] reads messages, dispatches78 ↓79apply_dispatch → apply_handle_insert / apply_handle_update / _delete80 ↓81subscriber-side execute the change (with conflict handling in82PG 18+)83 ↓84LogicalRepApplyLoop tracks progress via replorigin_session_setup85```8687## Slots — physical vs logical8889Both physical and logical replication use **replication slots** (`src/backend/replication/slot.c` — sibling, not in the logical/ subdir). Difference:9091- **Physical slot** — reserves WAL for a physical standby / walsender. No catalog_xmin. Just a `restart_lsn`.92- **Logical slot** — reserves WAL AND catalog snapshots (via `catalog_xmin`) for logical decoding. Requires more machinery — snapbuild state, output plugin, spill-file directory in `pg_replslot/<name>/`.9394Slot mismanagement is a common failure mode: a stale logical slot pins WAL indefinitely and blocks vacuum from removing dead catalog tuples. Monitor `pg_replication_slots.confirmed_flush_lsn` vs `pg_current_wal_lsn`.9596## Streaming vs non-streaming apply9798Non-streaming: the publisher accumulates a whole transaction in the reorderbuffer, sends everything on COMMIT. Simple, but slow for large transactions (memory pressure, spill).99100Streaming (PG 14+): publisher sends transaction changes as they decode (with a `Stream Start / Stream Stop` bracket), subscriber accumulates into a spool file until the streamed transaction commits, then applies.101102Parallel apply (PG 16+): with streaming + `streaming = parallel` on the subscription, the subscriber's launcher spawns leaf workers (`applyparallelworker.c`) that apply streamed transactions concurrently. Requires no schema-conflicting sub-transactions.103104## Conflict tracking (PG 18+)105106`conflict.c` implements the four conflict types the apply worker can detect:1071081. **`insert_exists`** — subscriber has a row with the same PK as the incoming INSERT.1092. **`update_missing`** — incoming UPDATE targets a row the subscriber doesn't have.1103. **`update_differ`** — target row exists but doesn't match the OLD tuple's values.1114. **`delete_missing`** — same shape for DELETE.112113Each logs a row into `pg_stat_subscription_conflicts` (via pgstat) and can trigger the subscription's `disable_on_error` policy.114115## Common patch shapes116117### Add a new logical replication protocol message118119Scenario exists: `knowledge/scenarios/add-new-replication-message.md`. Short:120- New byte-code in `proto.c` for the message shape.121- Encoder call from `pgoutput.c` (publisher).122- Decoder + dispatch in `worker.c` (subscriber).123- Version-gate via `LOGICAL_PROTO_VERSION_*` in `proto.h`.124125### Extend output-plugin callbacks126127- New callback in `include/replication/output_plugin.h` (e.g. `stream_change_cb`).128- Wire it in `logical.c`'s `LogicalDecodingContext.*` and the reorderbuffer replay path.129- Update `pgoutput.c` + `test_decoding` (in contrib) to implement it.130- Extension output plugins may need updates or fall back gracefully via NULL-callback checks.131132### Add a new conflict type133134- Extend `ConflictType` enum in `include/replication/conflict.h`.135- Detection + logging in `conflict.c` (`ReportApplyConflict`).136- Update `pg_stat_subscription_conflicts` schema + counter in `pgstat_subscription.c` (touches pgstat-framework).137138### Debug slot-not-advancing139140- Check `pg_replication_slots.confirmed_flush_lsn` — subscriber isn't acknowledging.141- Check subscriber's `pg_stat_subscription.last_msg_receipt_time` — is the apply worker running?142- Check `pg_stat_subscription.received_lsn` vs `pg_current_wal_lsn` on publisher — is receipt lagging?143- Check `pg_replication_slots.wal_status` — if `lost`, WAL was recycled.144145## Pitfalls146147- **`catalog_xmin` retention is the silent killer** — a logical slot that stops advancing prevents vacuum from removing dead catalog tuples. Cluster performance degrades silently over days.148- **Reorderbuffer spill files** — `pg_replslot/<name>/xid-*` files. If a slot stalls, spill can fill the disk. Not always obvious it's the slot's fault.149- **`snapbuild` needs a clean starting point** — creating a logical slot on a busy system may take a while (has to see all in-progress txns commit or abort before reaching CONSISTENT).150- **Origin sessions leak on error** — if apply worker crashes mid-transaction, `replorigin_session_setup` state may need manual reset via `pg_replication_origin_advance`.151- **Parallel apply requires no PROVIDED savepoints** — a transaction using SAVEPOINT can't be parallelized; the leaf worker will error and the coordinator will retry serially.152- **`decoding_context.streaming = false` is default** — a slot may not receive streamed messages unless the subscriber says `streaming = on`. Common misconfig.153- **Publisher-side vs subscriber-side `_PG_init`** — for output plugin extensions: publisher loads it via `output_plugin_options`; subscriber-side extensions load differently. Don't share state.154- **`pgoutput` vs `test_decoding` vs custom** — pgoutput speaks the wire format the subscriber expects; test_decoding is a debugging text serializer, NOT wire-compatible.155- **Slot-sync races on failover** — `slotsync.c` is a background worker on standby that replicates slot state. If the standby is promoted mid-sync, slots may lag or be inconsistent. Use `pg_sync_replication_slots()` explicitly before failover.156157## Related corpus158159- **Idioms** (9 covered): `apply-conflict-resolution`, `apply-handlers-insert-update-delete`, `apply-streaming-and-parallel`, `apply-worker-loop`, `apply-worker-loop-and-dispatch`, `logical-decoding-snapshot`, `output-plugin-callbacks`, `replication-origin-tracking`, `replication-slot-advance`.160- **Subsystem**: `replication` (the parent doc — covers physical + logical high-level).161- **Scenario**: `add-new-replication-message`.162- **Related idiom**: `tablesync-initial-copy` (COPY-based bootstrap for a new subscription).163- **Related session logs**: `2026-06-02-replication-synthesis.md`, `2026-06-04-a8-include-replication.md`.164165## Corpus-chain shortcut166167```168python3 scripts/corpus-chain.py --scenario add-new-replication-message169python3 scripts/corpus-chain.py --file src/backend/replication/logical/worker.c170python3 scripts/corpus-chain.py --idiom apply-worker-loop171```172173These surface the tight neighborhood: the 20 files under `logical/`, the 9 apply/decoding idioms, and the pgoutput integration point.174175## Boundary176177**Use this skill** for the `src/backend/replication/logical/` tree + `pgoutput` + apply-worker + reorderbuffer + slot management.178179**Don't use** for:180- **Physical streaming replication** — walsender/walreceiver/basebackup are the parent `replication` subsystem; different code paths, though they share slot infrastructure.181- **`walsender` / `walreceiver`** — those handle both physical and logical connections; use the parent `replication` subsystem doc.182- **Foreign data wrappers** (`postgres_fdw`) — that's not replication.183- **BDR / pglogical / Debezium** — external / contrib output plugins; they build ON logical decoding but their internals are their own.