The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Enable logical WAL | ALTER SYSTEM SET wal_level = 'logical'; | View examples |
| Inspect worker capacity | SHOW max_replication_slots; SHOW max_wal_senders; SHOW max_logical_replication_workers; | View examples |
| Publish explicit tables | CREATE PUBLICATION app_pub FOR TABLE app.customers, app.orders; | View examples |
| Restrict operations | CREATE PUBLICATION audit_pub FOR TABLE app.audit_log
WITH (publish = 'insert'); | View examples |
| Filter published rows | CREATE PUBLICATION eu_pub FOR TABLE app.accounts
WHERE (region = 'EU'); | View examples |
| Publish selected columns | CREATE PUBLICATION profile_pub FOR TABLE app.profiles (id, display_name, updated_at); | View examples |
| Choose replica identity index | ALTER TABLE app.events REPLICA IDENTITY
USING INDEX events_replica_key; | View examples |
| Use full-row identity | ALTER TABLE app.legacy_rows REPLICA IDENTITY FULL; | View examples |
| Create subscription | CREATE SUBSCRIPTION app_sub CONNECTION :'publisher_conninfo' PUBLICATION app_pub; | View examples |
| Create disabled subscription | CREATE SUBSCRIPTION app_sub CONNECTION 'host=publisher dbname=app user=logical_sub' PUBLICATION app_pub
WITH (connect=false); | View examples |
| Refresh table membership | ALTER SUBSCRIPTION app_sub REFRESH PUBLICATION
WITH (copy_data = true); | View examples |
| Stop subscription workers | ALTER SUBSCRIPTION app_sub DISABLE; | View examples |
| Keep table-owner execution | ALTER SUBSCRIPTION app_sub
SET (run_as_owner = false, password_required = true); | View examples |
| Inspect apply workers | SELECT subname,
worker_type,
pid,
received_lsn,
latest_end_lsn
FROM pg_stat_subscription; | View examples |
| Measure retained WAL | SELECT slot_name,
active,
wal_status,
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS retained_bytes
FROM pg_replication_slots; | View examples |
| Enable failover slot handling | ALTER SUBSCRIPTION app_sub SET (failover = true); | View examples |
Logical replication sends decoded row changes for selected tables in one database; it is not a byte-for-byte standby, a complete backup, or automatic high availability. Use it when table-level selection, cross-version migration, or independent subscriber schemas are requirements. Treat schema deployment, sequence state, conflict prevention, slot retention, credentials, and subscriber promotion as explicit parts of the design, then rehearse them with production-scale write rates before depending on the topology.
Step by step
Detailed examples
Choose logical replication for row changes, not cluster identity
The built-in pgoutput path decodes changes on a publisher and applies them to subscriber tables by name. It can cross major versions and operating systems and can select data, unlike physical streaming replication. It does not reproduce the publisher cluster, database roles, DDL, sequences, large objects, or every relation type. On PostgreSQL 18, the publisher needs wal_level=logical and enough replication slots and WAL senders; the subscriber needs enough logical replication workers, active replication origins, and general worker processes. Most capacity settings require a restart, so size them for subscriptions plus initial table-sync and parallel-apply workers.
-- Publisher; wal_level changes take effect after restart.
SHOW wal_level;
SHOW max_replication_slots;
SHOW max_wal_senders;
-- Subscriber; allow headroom beyond steady-state apply workers.
SHOW max_active_replication_origins;
SHOW max_logical_replication_workers;
SHOW max_worker_processes; Note: Defaults may demonstrate the feature but are not a capacity plan. Include table synchronization and parallel apply in the worker budget.
Publish an intentional and reviewable change set
A publication defines tables and operation types; it does not copy schema or store data itself. Prefer explicit tables when ownership is distributed, because automatic schema-wide membership can include a future table created by another owner. Row filters are evaluated on the publisher and affect the initial copy, but they do not filter TRUNCATE. For UPDATE or DELETE, filter and column-list requirements are constrained by replica identity. Column lists reduce payload and subscriber schema requirements, but PostgreSQL explicitly warns that they do not protect secrets from a malicious subscriber.
CREATE PUBLICATION customer_eu_pub
FOR TABLE app.customers (customer_id, region, display_name, updated_at)
WHERE (region = 'EU')
WITH (publish = 'insert, update, delete');
SELECT pubname, pubinsert, pubupdate, pubdelete, pubtruncate
FROM pg_publication
WHERE pubname = 'customer_eu_pub'; Note: customer_id and region must satisfy the replica-identity rules for published updates and deletes. Test transitions into and out of the filter: PostgreSQL can transform such updates into inserts or deletes.
Give every mutable published table a dependable row identity
INSERT needs no old-row lookup, but UPDATE and DELETE need a replica identity so the subscriber can locate the target row. A primary key is the default. A chosen replica-identity index must meet PostgreSQL's eligibility rules; REPLICA IDENTITY FULL records old column values and can increase WAL volume and subscriber lookup work. FULL also has data-type limitations for applying updates and deletes. Prefer a narrow, stable, non-null key and verify that subscriber-side uniqueness matches the intended conflict model.
ALTER TABLE app.events
ALTER COLUMN tenant_id SET NOT NULL,
ALTER COLUMN event_id SET NOT NULL;
CREATE UNIQUE INDEX events_replica_key
ON app.events (tenant_id, event_id);
ALTER TABLE app.events
REPLICA IDENTITY USING INDEX events_replica_key;
SELECT n.nspname, c.relname, c.relreplident
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE n.nspname = 'app' AND c.relname = 'events'; Note: Setting NOT NULL validates existing data and takes a strong table lock, so plan it separately on a busy table. The replica index must be unique, immediate, non-partial, and use only NOT NULL columns.
Stage subscriber schema and initial synchronization deliberately
CREATE SUBSCRIPTION normally connects immediately, creates a logical slot, copies existing rows, and starts applying changes. Target tables must already exist with compatible names and published columns; indexes and constraints affect copy speed and conflict behavior. Initial copy creates additional synchronization workers and temporary slots, so estimate source read load, network volume, target writes, WAL retention, and lock effects. Use connect=false or create_slot=false only with a documented manual-slot sequence—these options change other defaults and a missing or mismatched slot can lose continuity.
-- In psql, obtain the complete conninfo through a protected deployment channel.
\prompt -s 'Publisher conninfo: ' publisher_conninfo
CREATE SUBSCRIPTION app_sub
CONNECTION :'publisher_conninfo'
PUBLICATION app_pub
WITH (copy_data = true, streaming = parallel, run_as_owner = false);
\unset publisher_conninfo
SELECT srrelid::regclass AS table_name, srsubstate
FROM pg_subscription_rel
WHERE srsubid = (SELECT oid FROM pg_subscription WHERE subname = 'app_sub'); Note: For a non-superuser-owned PostgreSQL 18 subscription with password_required=true, the password must be in conninfo and is stored in pg_subscription; inject it through a protected administrative channel and tightly restrict catalog access.
Coordinate DDL, table membership, and conflict recovery
Logical replication does not propagate DDL. Apply compatible additive changes to the subscriber before publisher writes require them, then deploy the publisher change; destructive or type-changing migrations need a rehearsed compatibility sequence. ALTER PUBLICATION membership changes do not automatically update subscriptions until REFRESH PUBLICATION. Subscriber writes, duplicate keys, missing rows, constraints, triggers configured for replica mode, and permissions can stop apply. Diagnose the subscriber log and conflict statistics, fix the cause, and resume without casually skipping a transaction, which creates divergence.
SELECT p.pubname, n.nspname, c.relname
FROM pg_publication_rel AS pr
JOIN pg_publication AS p ON p.oid = pr.prpubid
JOIN pg_class AS c ON c.oid = pr.prrelid
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE p.pubname = 'app_pub'
ORDER BY n.nspname, c.relname;
-- Run on the subscriber after compatible target tables exist.
ALTER SUBSCRIPTION app_sub REFRESH PUBLICATION WITH (copy_data = true); Make both replication identities least-privileged
The publisher connection role needs LOGIN, REPLICATION, pg_hba.conf access, and SELECT for initial copies; use SCRAM authentication and verified TLS on untrusted networks. The subscription owner is a separate local privilege boundary. PostgreSQL 18 defaults run_as_owner to false, applying as each target table owner; the subscription owner therefore needs SET ROLE rights to those owners. This is safer than letting table owners execute code as a powerful subscription owner. Restrict table ownership and trigger creation, avoid superuser ownership, protect connection strings, and understand row-level-security behavior before enabling a topology.
-- Publisher, in psql. Add a narrow pg_hba.conf rule separately.
CREATE ROLE logical_sub LOGIN REPLICATION;
\password logical_sub
GRANT CONNECT ON DATABASE app TO logical_sub;
GRANT USAGE ON SCHEMA app TO logical_sub;
GRANT SELECT ON app.customers, app.orders TO logical_sub;
-- Subscriber: retain the safer PostgreSQL 18 execution default.
ALTER SUBSCRIPTION app_sub
SET (run_as_owner = false, password_required = true); Note: Do not paste a real password into migration history. Validate current PostgreSQL security documentation when publisher roles do not have BYPASSRLS or when target tables use row-level security.
Monitor apply progress and slot retention as separate risks
pg_stat_subscription shows currently active subscriber workers; zero rows for an enabled subscription can indicate a crashed worker. On the publisher, pg_replication_slots exposes the oldest WAL still required. An inactive or stalled logical slot can retain enough WAL to fill pg_wal; max_slot_wal_keep_size and idle_replication_slot_timeout can limit exposure but may invalidate the slot and force resynchronization. Alert on worker absence, table-sync state, apply errors, slot activity, retained bytes, wal_status, safe_wal_size, disk headroom, and end-to-end data freshness.
-- Subscriber
SELECT subname, worker_type, pid, received_lsn, latest_end_lsn, latest_end_time
FROM pg_stat_subscription
ORDER BY subname, worker_type;
-- Publisher
SELECT slot_name, active, restart_lsn, confirmed_flush_lsn, wal_status, safe_wal_size,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots
WHERE slot_type = 'logical'; Note: A byte distance is not a time guarantee. Add an application-level freshness probe for the records your consumers actually require.
Engineer logical continuity across physical failover
A subscriber cannot safely continue from a newly promoted publisher merely because the tables are present: its logical slot state must also be available and sufficiently advanced. PostgreSQL 17 and later provide logical failover-slot synchronization building blocks, and PostgreSQL 18 subscriptions can mark associated slots with failover=true. The physical standby still needs the documented slot-synchronization configuration, WAL retention, connectivity, and readiness checks. This does not detect failures, promote a server, fence the old primary, redirect clients, or resolve divergent writes; an HA controller and a tested runbook remain necessary.
-- Subscriber: inspect whether the subscription requests failover handling.
SELECT subname, subenabled, subfailover
FROM pg_subscription
WHERE subname = 'app_sub';
-- Candidate publisher standby: verify the synchronized logical slot.
SELECT slot_name, slot_type, failover, synced, invalidation_reason, confirmed_flush_lsn
FROM pg_replication_slots
WHERE slot_name = 'app_sub'; Note: Columns and failover-slot procedures are version-specific. Follow the documentation for the exact major release and never promote until slot synchronization and old-primary fencing are verified.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
- PostgreSQL Global Development GroupPostgreSQL: Logical Replicationpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: CREATE PUBLICATIONpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: CREATE SUBSCRIPTIONpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: Logical Replication Securitypostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: Logical Replication Monitoringpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: Logical Replication Failoverpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: Replication Slotspostgresql.org
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



