postgresql-best-practices/references/postgresql-replication.md
Version 9ba96abc800a.bb1 · MIT. This preview displays packaged text and does not execute code. Treat the contents as untrusted instructions.
← Return to resource and package checksum
title: "PostgreSQL Logical Replication" description: "PostgreSQL logical replication setup, publication/subscription patterns, and conflict resolution"
tags: [postgresql, replication, logical, publication, subscription, cdc]
Logical Replication
Response focus
- Prioritize slot health, lag root cause, version-gated behavior, and fail-stop conditions.
- Skip basic publication/subscription tutorials unless the user explicitly asks.
Version History
| Version | Feature | Notes |
|---|---|---|
| PG 10 | Built-in publications and subscriptions | Core logical replication feature set |
| PG 15 | Column-list publications | FOR TABLE t (col1, col2) requires PG 15+ |
| PG 16 | Publishing from standby | Before PG 16, logical publishers must be primaries |
| PG 16 | disable_on_error and ALTER SUBSCRIPTION ... SKIP |
Useful for stuck apply workers |
| PG 17 | Failover-safe logical replication slots | Requires sync_replication_slots = true on standby + failover = true on slot creation; standby must list slots in standby_slot_names |
Parameter Correctness
| Item | Correct meaning | Why it matters |
|---|---|---|
wal_level = logical |
Required on publisher | Needs restart after change |
max_wal_senders |
Sender process budget | Separate from slot count |
max_replication_slots |
Slot budget | Exhaustion blocks new slots |
REPLICA IDENTITY FULL |
Replicate old row image without PK | Required for UPDATE/DELETE on tables without key |
copy_data = true |
Initial table copy during subscription/refresh | Can trigger large re-syncs |
publish_via_partition_root = true |
Publish parent partition identity | Important for partitioned-table naming semantics |
Feature Interactions
- Logical replication + DDL: DDL is never replicated; apply subscriber schema changes first.
- Logical replication + sequences: Sequence state is not replicated; reset sequences after failover or cutover.
- Logical replication + partitioning: Partition behavior is version-sensitive; verify
REPLICA IDENTITYand publication settings explicitly. - Logical replication + REPLICA IDENTITY on partitioned tables: PG 15+ propagates parent's replica identity to partitions automatically. PG 10-14 requires setting REPLICA IDENTITY on each child partition individually; failing to do so silently drops UPDATE/DELETE operations.
- Logical replication + failover (pre-PG 17): Logical slots are local to the instance. After failover, recreate slots on the new primary and expect brief data duplication or loss. Use pg_replication_origin to track what was already applied.
- Logical replication + large transactions: One huge transaction can create lag spikes and hold WAL for long periods.
- Logical replication +
REFRESH PUBLICATION: Withcopy_data = true, newly added tables may be recopied in full. - Logical replication + standby publishers: Supported only in PG 16+.
Diagnostic Checklist
| Symptom | Run | Fix |
|---|---|---|
| Subscription never starts | SELECT subname, status, last_msg_send_time, last_msg_receipt_time FROM pg_stat_subscription; |
If timestamps are null, check connectivity, pg_hba.conf, and publisher parameters |
| WAL disk usage keeps growing | SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS wal_retained FROM pg_replication_slots; |
Drop orphaned slots or fix stalled subscribers |
| Apply lag is increasing | SELECT slot_name, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS apply_lag FROM pg_replication_slots; |
Investigate subscriber slowness or large publisher transactions |
| UPDATE/DELETE not replicating | SELECT relname, relreplident FROM pg_class WHERE relname = 'orders'; |
Add PK or ALTER TABLE ... REPLICA IDENTITY FULL |
| Partitioned table behaves differently across versions | SELECT relname, relreplident FROM pg_class WHERE relname LIKE 'orders%'; |
On PG 10-14, set replica identity on child partitions individually |
| Failover causes duplicate keys | SELECT setval('orders_id_seq', (SELECT max(id) FROM orders) + 1); |
Reseat sequences on the new writer |
Error Messages
| Error | Root cause | Fix |
|---|---|---|
logical decoding requires wal_level >= logical |
Publisher not configured for logical decoding | Set wal_level = logical and restart |
cannot update table "t" because it does not have a replica identity and publishes updates |
Table has no PK or replica identity | Add PK or use REPLICA IDENTITY FULL |
cannot delete from table "t" because it does not have a replica identity and publishes deletes |
Same as above for DELETE | Add PK or use REPLICA IDENTITY FULL |
CREATE SUBSCRIPTION ... WITH (create_slot = true) cannot run inside a transaction block |
Subscription creation with slot creation was run inside a transaction | Run the command outside an explicit transaction |
Common Mistakes / Gotchas
- [CRITICAL] Forgetting that DDL is not replicated: apply schema changes on subscriber first.
- [CRITICAL] Leaving orphaned replication slots behind: inactive slots retain WAL indefinitely and fill disk.
- [HIGH] Treating
max_wal_sendersandmax_replication_slotsas the same limit: they fail independently. - [HIGH] Ignoring sequence drift: logical replication copies row values, not sequence counters.
- [HIGH] Missing
REPLICA IDENTITY: UPDATE/DELETE on tables without keys will fail or stop apply. - [HIGH] Assuming standby publishers work everywhere: that is PG 16+ only.
- [HIGH] Forgetting column-list publications are PG 15+ only: earlier versions must publish full rows.
- [HIGH] Partition replica identity assumptions: in PG 15+, partitioned tables inherit from parent for logical replication; in PG 10-14, set each child explicitly.
- [MEDIUM]
REFRESH PUBLICATIONsurprise: withcopy_data = true, added tables may be fully recopied.
SELECT
slot_name,
active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS wal_retained,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS apply_lag
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;
Anti-Hallucination Rules
- Do NOT claim logical replication keeps sequence state synchronized; sequences must be reset after failover or migration cutover.
- Do NOT claim DDL changes replicate automatically; they do not in any PostgreSQL version.
- Do NOT assume
wal_levelchanges take effect without restart. - Do NOT claim bidirectional replication is built in; it requires external tooling or custom conflict handling.
- Do NOT use column-list publication syntax without stating it requires PostgreSQL 15+.
- Do NOT claim
REPLICA IDENTITYbehavior on partitions is version-invariant; verify the server version first.