Build a disposable PostgreSQL 18 publisher/subscriber pair and trace logical decoding from WAL through pgoutput, publications, subscriptions, replica identity, logical slots, and replication origins.

Logical Decoding Mental Model, Replication Identity, Publications, and Subscriptions

Build a disposable PostgreSQL 18 publisher/subscriber pair and trace logical decoding from WAL through pgoutput, publications, subscriptions, replica identity, logical slots, and replication origins.

Intermediate → Advanced180–240 minutesLogical replication and data-movement labCurrent patched PostgreSQL 18.xCore PostgreSQL only for mandatory workDisposable local publisher/subscriber on ports 55439–55440Optional third node on port 55441 only where explicitly statedReplication, database CREATE, and table ownership privileges where statedLast reviewed: August 2026

Learning outcomes

ServiceHub now needs to move a selected operational dataset into another PostgreSQL database without copying an entire physical cluster. The publisher may remain writable while a subscriber receives changes, and the two databases may even run different PostgreSQL major versions during a migration. This is the problem space for logical replication.

Logical replication is not “SQL statement replay.” PostgreSQL reads Write-Ahead Log (WAL), logically decodes row changes, filters them through a publication, sends them through the streaming replication protocol, and applies them to same-named target tables on the subscriber. The lab uses two disposable PostgreSQL 18 clusters: publisher port 55439 and subscriber port 55440.

01

Explain logical decoding, pgoutput, publications, subscriptions, slots, and replication origins as distinct parts of one pipeline.

02

Configure a publisher with wal_level=logical and a least-privilege LOGIN+REPLICATION connection role.

03

Explain why UPDATE and DELETE require a usable replica identity.

04

Create publication/subscription objects and observe pg_stat_subscription and pg_replication_slots.

05

Separate data replication from schema replication, backup, auditing, and application-level CDC semantics.

Mental model

Physical replication reproduces PostgreSQL storage changes. Logical replication reproduces selected table changes identified by logical row identity. A publication describes what can leave a publisher database; a subscription describes what a subscriber database consumes.

1. From WAL record to applied row

On the publisher, a WAL sender starts logical decoding. PostgreSQL's built-in logical replication uses the standard pgoutput output plugin. The plugin turns WAL-decoded changes into the logical replication protocol and applies publication rules such as table membership, row filters, column lists, and published operation types. The subscriber's leader apply worker receives those changes and applies them in transactional order.

sql · inspect logical-replication capacity settings
SELECT name, setting, context, source, pending_restartFROM pg_settingsWHERE name IN (  'wal_level','max_wal_senders','max_replication_slots',  'max_logical_replication_workers','max_sync_workers_per_subscription',  'max_parallel_apply_workers_per_subscription','max_active_replication_origins')ORDER BY name;

The publisher requires wal_level=logical. It also needs enough WAL senders and replication slots for subscriptions and initial table-synchronization workers. On the subscriber, logical-replication workers compete for the global max_worker_processes budget. Capacity settings are limits, not targets to maximize.

2. Build the disposable publisher and subscriber

shell · initialize two local PostgreSQL 18 clusters
initdb -D ./ch15_pubinitdb -D ./ch15_subpg_ctl -D ./ch15_pub -o "-p 55439" -l ./ch15_pub.log startpg_ctl -D ./ch15_sub -o "-p 55440" -l ./ch15_sub.log start
sql · publisher: enable logical WAL and restart-required capacity
ALTER SYSTEM SET wal_level = 'logical';ALTER SYSTEM SET max_wal_senders = '10';ALTER SYSTEM SET max_replication_slots = '10';

Restart the publisher after those settings if pending_restart is true. Do not change the long-lived ServiceHub lab cluster for this exercise; the chapter deliberately uses separate data directories.

psql · publisher: create the database, owner, replication role, and source table
CREATE DATABASE servicehub_logical_pub;\c servicehub_logical_pubCREATE ROLE ch15_pub_owner NOLOGIN;CREATE ROLE ch15_repl LOGIN REPLICATION;-- Set a disposable lab password interactively:\password ch15_replCREATE SCHEMA app AUTHORIZATION ch15_pub_owner;CREATE TABLE app.ch15_work_orders (  work_order_id bigint PRIMARY KEY,  customer_id   bigint NOT NULL,  region        text NOT NULL,  status        text NOT NULL,  labor_minutes integer NOT NULL CHECK (labor_minutes >= 0),  changed_at    timestamptz NOT NULL DEFAULT clock_timestamp());ALTER TABLE app.ch15_work_orders OWNER TO ch15_pub_owner;INSERT INTO app.ch15_work_ordersVALUES  (15001, 501, 'north', 'queued',      0, clock_timestamp()),  (15002, 502, 'south', 'assigned',   15, clock_timestamp()),  (15003, 503, 'north', 'in_progress',40, clock_timestamp());GRANT CONNECT ON DATABASE servicehub_logical_pub TO ch15_repl;GRANT USAGE ON SCHEMA app TO ch15_repl;GRANT SELECT ON app.ch15_work_orders TO ch15_repl;

The replication connection role needs LOGIN and REPLICATION, a matching pg_hba.conf rule, and SELECT on published tables for initial copy. In a real environment, use SCRAM, certificates, or another approved secret-distribution mechanism; never commit a subscription connection password to source control.

3. Replica identity answers “which target row?”

For inserts, PostgreSQL only needs the new row. For updates and deletes, the subscriber must identify which existing target row corresponds to the source change. By default the primary key is the replica identity. A qualifying unique index can be designated instead. REPLICA IDENTITY FULL sends the old row as identity, but it can be much more expensive and should be a fallback, not the default design.

sql · deliberately wrong: publish an update/delete table with no identity
CREATE TABLE app.ch15_identity_bad (  note text,  status text);CREATE PUBLICATION ch15_bad_pubFOR TABLE app.ch15_identity_bad;INSERT INTO app.ch15_identity_bad VALUES ('x','new');-- Expected to fail because the publication publishes UPDATE by default:UPDATE app.ch15_identity_badSET status = 'done'WHERE note = 'x';

The publisher rejects that update because the table has no replica identity while its publication includes updates. Repair the model with a durable business key or primary key rather than hiding the problem with FULL unless there is a deliberate reason.

sql · repair the identity
ALTER TABLE app.ch15_identity_bad  ADD COLUMN identity_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY;UPDATE app.ch15_identity_badSET status = 'done'WHERE note = 'x';DROP PUBLICATION ch15_bad_pub;DROP TABLE app.ch15_identity_bad;

4. Create publication and target schema

sql · publisher: publication and catalog evidence
CREATE PUBLICATION ch15_servicehub_pubFOR TABLE app.ch15_work_orders;SELECT p.pubname, p.pubinsert, p.pubupdate, p.pubdelete, p.pubtruncateFROM pg_publication AS pWHERE p.pubname = 'ch15_servicehub_pub';SELECT pubname, schemaname, tablenameFROM pg_publication_tablesWHERE pubname = 'ch15_servicehub_pub';

A publication does not copy schema definitions. The subscriber must already contain the target table with the same fully-qualified name. Column order can differ, but column names and convertible data types must line up with what is published.

psql · subscriber: create a non-superuser subscription owner and target table
CREATE DATABASE servicehub_logical_sub;\c servicehub_logical_subCREATE ROLE ch15_sub_owner LOGIN;GRANT pg_create_subscription TO ch15_sub_owner;GRANT CREATE ON DATABASE servicehub_logical_sub TO ch15_sub_owner;CREATE SCHEMA app AUTHORIZATION ch15_sub_owner;SET ROLE ch15_sub_owner;CREATE TABLE app.ch15_work_orders (  work_order_id bigint PRIMARY KEY,  customer_id   bigint NOT NULL,  region        text NOT NULL,  status        text NOT NULL,  labor_minutes integer NOT NULL CHECK (labor_minutes >= 0),  changed_at    timestamptz NOT NULL);RESET ROLE;

PostgreSQL 18 allows a role with membership in pg_create_subscription plus database CREATE privilege to create a subscription. Apply privileges remain a separate concern. In this lab the subscription owner also owns the target table, avoiding an unnecessary superuser apply path.

5. Create the subscription and observe the slot

sql · subscriber: create subscription as the target owner
SET ROLE ch15_sub_owner;CREATE SUBSCRIPTION ch15_servicehub_subCONNECTION 'host=127.0.0.1 port=55439 dbname=servicehub_logical_pub user=ch15_repl password=<LAB_PASSWORD> application_name=ch15_servicehub_sub'PUBLICATION ch15_servicehub_pubWITH (copy_data = true, streaming = parallel);RESET ROLE;

The streaming default is parallel in PostgreSQL 18, but the lab states it explicitly. Initial table synchronization takes a source snapshot, copies existing rows, then catches that table up to the leader apply worker before handing it to normal ongoing replication.

sql · subscriber: monitor worker state
SELECT subname, worker_type, pid, relid::regclass,       received_lsn, latest_end_lsn,       last_msg_receipt_time, latest_end_timeFROM pg_stat_subscriptionWHERE subname = 'ch15_servicehub_sub'ORDER BY worker_type, relid;SELECT s.subname, r.srrelid::regclass AS relation,       r.srsubstate, r.srsublsnFROM pg_subscription_rel AS rJOIN pg_subscription AS s ON s.oid = r.srsubidWHERE s.subname = 'ch15_servicehub_sub';
sql · publisher: inspect the logical slot created for the subscription
SELECT slot_name, slot_type, plugin, database, active,       restart_lsn, confirmed_flush_lsn, wal_status,       safe_wal_size, inactive_since, invalidation_reasonFROM pg_replication_slotsWHERE slot_type = 'logical';

The subscription's main logical slot retains WAL until decoded changes are confirmed. A disabled, broken, or abandoned subscription can therefore become a disk-capacity problem on the publisher. Slot lifecycle belongs in every logical-replication runbook.

6. Prove data movement and origin tracking

sql · publisher: generate an update and insert
UPDATE app.ch15_work_ordersSET status = 'completed',    labor_minutes = 62,    changed_at = clock_timestamp()WHERE work_order_id = 15002;INSERT INTO app.ch15_work_ordersVALUES (15004, 504, 'west', 'queued', 0, clock_timestamp());
sql · subscriber: prove source state arrived
SELECT work_order_id, customer_id, region, status, labor_minutesFROM app.ch15_work_ordersORDER BY work_order_id;SELECT external_id, remote_lsn, local_lsnFROM pg_replication_origin_statusORDER BY external_id;

Replication-origin progress prevents a logical consumer from repeatedly applying the same remote progress point and supports more advanced replication-loop controls. Do not treat origin metadata as an application audit trail; it is replication progress state.

What this proves

Matching source and target rows plus advancing subscription/origin LSNs prove that the current publication/subscription path is applying changes. They do not prove that DDL, sequences, large objects, privileges, configuration, or every business invariant is synchronized.

7. Verification and cleanup

For production acceptance, verify data, worker health, slot retention, permissions, logs, and schema compatibility. For this disposable chapter lab, keep the topology for Lessons 2–5. If you need to remove it, drop the subscription before deleting publisher slots so PostgreSQL can clean up its remote resources normally.

sql · subscriber: controlled teardown when the chapter is finished
ALTER SUBSCRIPTION ch15_servicehub_sub DISABLE;DROP SUBSCRIPTION ch15_servicehub_sub;DROP DATABASE servicehub_logical_sub WITH (FORCE);

Check your understanding

  1. Why does logical replication need a replica identity for UPDATE and DELETE?
  2. What role does pgoutput play?
  3. Why must the target table already exist on the subscriber?
  4. What does a logical replication slot protect, and what new risk can it create?
  5. Why is replication-origin state not a business audit log?
Review the answers

UPDATE and DELETE need a stable way to identify the subscriber row. pgoutput converts logically decoded WAL changes into PostgreSQL's logical replication protocol and applies publication filtering. Built-in logical replication does not replicate schema definitions, so targets must exist. A slot protects required WAL from recycling but can retain excessive WAL if its consumer stops. Replication origins track apply progress; they do not record complete business history.

Authoritative references

Logical replication is version-, privilege-, topology-, and schema-sensitive. These PostgreSQL 18 primary sources define the behavior used in this lesson.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.