Updated: 2026-07-07

PostgreSQL 16, released in September 2023, didn’t ship a headline feature so much as something far more valuable to anyone running databases in production. It closed, one by one, the four or five cracks that had turned logical replication into an exercise in patience (full rundown in what changed in PG16 day to day). Today, for cross-version migrations, geo-distributed architectures with local reads, or reasonable CDC pipelines, the correct answer nearly always begins with the logical replication built into the database itself.

Key takeaways

  • Parallel apply on the subscriber: apply runs close to 2x faster on large transactions vs PG15.

  • Physical standbys can expose logical slots of their own, offloading the primary.

  • Bidirectional replication adds the scaffolding but does not resolve conflicts automatically: primary-key partitioning discipline is required.

  • Conflicts stop the subscription and emit the exact LSN: ALTER SUBSCRIPTION ... SKIP or DISABLE/ENABLE to resolve.

  • Logical replication does not replicate DDL or sequences: explicit plan required for every migration.

Logical vs physical: the usual misunderstanding

The most common confusion is treating both modes as interchangeable. They aren’t. Physical replication ships raw WAL to a byte-identical replica: it’s the foundation of hot standbys, failover, and nearly all serious high availability. Logical replication, instead, decodes that WAL and re-expresses it as high-level operations (INSERT, UPDATE, DELETE, TRUNCATE) tied to a publication. That decoding carries a CPU cost the physical path doesn’t pay, but in exchange it enables replication between different versions, to targets with a different schema, or to consumers that aren’t even PostgreSQL.

They are complementary tools: nobody should tear out the physical standby to run everything on logical replication.

The cracks that closed

Parallel apply: up to PG15, subscriber apply was strictly sequential. A single large transaction could block the entire queue for minutes. PG16 introduces parallel workers controlled by max_parallel_apply_workers_per_subscription, defaulting to 2. pgEdge’s benchmarks on the patch put the improvement at roughly 2x in elapsed time for large transactions.

Slots on standbys: before PG16, only the primary could expose logical slots. From PG16 on, a physical standby can expose its own slots (backed by the new pg_log_standby_snapshot() function) and feed downstream consumers, enabling cleaner fan-out architectures: one primary, regional physical standbys, and, hanging off each of them, the logical subscribers for that region. It’s the same decentralization pattern that Citus pursues when scaling Postgres horizontally, applied here to reading changes rather than partitioning data.

Bidirectional replication: not automatic multi-master. PostgreSQL still doesn’t resolve conflicts for you. What’s added is the scaffolding to build it without external extensions, provided the application respects primary-key partitions that prevent simultaneous writes to the same row on two nodes. Without that discipline, you end up with silent divergence.

Publications, subscriptions, and conflict resolution

The conceptual model is still the one from PG10: on the source you create a publication declaring which tables go out, and on the target a subscription pointing at that publication:

CREATE PUBLICATION mypub FOR TABLE users, orders;

CREATE SUBSCRIPTION mysub
  CONNECTION 'host=primary port=5432 dbname=mydb user=replicator password=secret'
  PUBLICATION mypub
  WITH (streaming = parallel);

SELECT subname, received_lsn, latest_end_lsn, latest_end_time
FROM pg_stat_subscription;

Conflicts deserve their own paragraph. PG16 does not automatically resolve a primary-key collision between a local INSERT and one arriving through replication: it stops the subscription, marks it as errored, and emits the exact LSN in the log. The operation is a manual decision: fix the source, skip the transaction with ALTER SUBSCRIPTION ... SKIP (lsn = ...), or disable it with DISABLE and re-enable after the repair. See also PostgreSQL 17’s vacuum and logical replication improvements for the next step in this evolution.

What remains out of scope

An honest picture requires listing what PG16 doesn’t do:

  • No DDL replication: an ALTER TABLE ADD COLUMN must be applied manually on both sides, in order.

  • Sequences don’t sync automatically: after a cutover you need to advance them with setval.

  • Large objects (lo_*) remain out of scope.

  • TRUNCATE has replicated since PG11, but it carries caveats when foreign keys span published and non-published tables.

For automatic DDL, deterministic conflict resolution, or genuine multi-master, extensions such as pgLogical or enterprise solutions like BDR remain the answer.

Operations and migrations

Logical replication shines in two contexts.

Minimum-downtime migration between major versions: you establish the subscription from a PG14/15 source to a freshly provisioned PG16 target, let it converge, pause the application for a few seconds of final catch-up, switch the connection string, and decommission the old one. Minutes of window versus the hours of a classic pg_dump.

CDC into a data warehouse: Debezium subscribes to the logical slot and pushes changes to Kafka, from where it feeds any analytical target without touching the transactional one. To monitor slot state and apply lag, the key metrics are pg_replication_slot_retained_wal_bytes and the delta between confirmed_flush_lsn and pg_current_wal_lsn(). An orphaned slot can fill the primary’s disk in hours, so alerting on WAL retention is a priority.

PostgreSQL 16 doesn’t turn logical replication into a magic switch; what it does is remove the technical excuses that for years kept it relegated to marginal cases.

This article is also available in Spanish.

Sources

  1. PostgreSQL — official PostgreSQL 16 release notes
  2. Crunchy Data — Logical Replication on Standbys in Postgres 16
  3. pgEdge — PostgreSQL 16 Logical Replication Improvements in Action
  4. Debezium — PostgreSQL connector reference documentation