Logical replication is PostgreSQL's answer to selective database synchronization: replicate specific tables, not the whole database; run on different major versions, not just binary-compatible replicas; and stream changes as logical events, not physical WAL, making it the natural foundation for change data capture (CDC) pipelines. Unlike physical streaming replication (which replicates every byte of the WAL to a standby), logical replication decodes the WAL into row-level insert/update/delete events, filters them by table and publication, and streams them to subscribers that may be a different database, a different PostgreSQL version, or a non-database target (Kafka, data warehouse, search index) via Debezium. This piece explains the architecture (replication slots, logical decoding, publications, subscriptions), walks through setup and configuration, covers the operational hazards (slot lag, schema evolution, replica lag), compares it to physical replication and alternatives like Debezium-directly, and sets expectations for what logical replication is and is not good for.

What is logical replication and why it matters

Logical replication solves a specific, recurrent need: keep a downstream database or system synchronized with a PostgreSQL source without coupling to physical replication or the cost of polling. Traditional physical streaming replication offers high availability and read scaling, but it requires binary compatibility (same major version, same architecture), replicates every byte (no selectivity), and creates true replicas (not suitable for ETL or transformation). Polling (periodically asking 'what changed since I last looked?') is simpler to build but introduces lag, inefficiency, and missed changes on failure. Logical replication strikes a middle ground: it reads the WAL (the same authoritative change log physical replication uses) and decodes it into logical row-level events (insert/update/delete), allowing a consumer to subscribe to specific tables, transform them, and apply them to a different target—without the overhead of triggers or polling.

The defining use cases are CDC (feeding downstream systems like Kafka, search indexes, or data warehouses with database changes), cross-version migration (upgrading from PostgreSQL 12 to 16 by replicating to the new version in parallel), selective replication (replicating only critical tables to a read-only replica or analytics database), and multi-region synchronization (keeping data consistent across geographically distributed PostgreSQL instances without the latency of distributed transactions). Logical replication is not a replacement for physical replication for high-availability standby—it is higher-latency, requires more CPU on the publisher, and is not suitable for zero-downtime failover—but it is the right tool for database-to-pipeline and database-to-database synchronization when the target is different in some way (version, schema, location, or type).

Architecture: publications, subscriptions, replication slots, and logical decoding

The logical replication pipeline has four moving parts. Publications define what to replicate: a publication on the source database specifies tables and (optionally) row filters and column lists. Think of it as 'publish everything from the users table where status != ''deleted''' or 'publish the id and name columns, not the password hash'. A publication is connected to the WAL—any INSERT, UPDATE, or DELETE that matches the publication's rules is eligible to be streamed.

Replication slots are the reliability mechanism. A slot tracks a consumer's position in the WAL, ensuring the database retains WAL from that position forward. If a subscriber disconnects, it reconnects to the same slot and resumes from where it left off—no missed changes. The tradeoff is the slot-lag hazard: a slow or stopped consumer prevents WAL from being discarded, causing the WAL to accumulate and eventually fill the database's disk. Logical replication adds a replication slot for each subscription, and monitoring slot lag is a critical operational concern.

Logical decoding is the engine that reads the WAL and transforms it. The WAL contains physical records (byte-level changes to disk pages), but consumers care about logical changes (rows inserted, updated, deleted). Logical decoding interprets the WAL into logical events: it reconstructs which row changed, what the before-image and after-image were (if relevant), which table, which columns, and the transaction boundary. This decode happens on the source, and the events are streamed to subscribers over network connections.

Subscriptions are the consumer side. A subscription on a target database (often the same PostgreSQL version, sometimes different, or a Debezium connector pulling into Kafka) connects to a publication on a source and applies the decoded changes. The subscriber can be configured to apply changes immediately, or to skip initial snapshot and stream only incremental changes. Multiple subscribers can pull from the same publication, and a subscription can have filters or column lists to narrow the schema further.

Setup: configuring WAL, publications, and subscriptions

To enable logical replication, first set wal_level = logical on the source database. This is the primary one-time configuration change: it increases WAL verbosity to include enough information for logical decoding (physical replication uses wal_level = replica, which logs less). Restart the database to apply it.

On the source, create a publication specifying tables and filters:

CREATE PUBLICATION my_pub FOR TABLE users, orders WHERE (status != 'archived');

This publication will stream inserts, updates, and deletes on the users and orders tables (except rows where status is 'archived'). Logical replication is selective at the table level; row filters are a PostgreSQL 15+ feature.

On the target (subscriber), create a subscription pointing to the publication on the source:

CREATE SUBSCRIPTION my_sub CONNECTION 'host=source dbname=mydb user=repl password=...' PUBLICATION my_pub;

The subscription immediately attempts to connect to the source and begin replicating. If it is the first subscription for this publication, the source will snapshot the tables (copy the current state) to the subscriber, then stream incremental changes. If snapshots are not desired (the subscriber already has a copy), you can disable snapshots with copy_data = false.

Both source and subscriber need a replication-capable database user with REPLICATION attribute and sufficient SELECT/INSERT/UPDATE/DELETE permissions on the affected tables. Network access must be configured (typically a dedicated replication user in pg_hba.conf).

How logical replication differs from physical replication

Physical streaming replication (using wal_level = replica) ships the entire WAL to a standby, which replays it byte-for-byte, creating a binary-identical copy. Logical replication decodes the WAL into logical events and allows the subscriber to filter, transform, or apply changes selectively. The differences matter:

AspectPhysicalLogical
SelectivityAll changes (whole database)Specific tables and rows
Target versionSame major versionDifferent major versions (15→16)
Target systemPostgreSQL replicaAny system (Kafka, data warehouse, app)
Latency~milliseconds~seconds (decoding overhead)
CPU overhead on sourceLow (just WAL shipping)Higher (decoding)
Use caseHigh-availability standby, read scalingCDC, cross-version migration, selective sync

Physical replication is the default for failover; logical replication is the building block for CDC and multi-target synchronization. Many setups use both: physical replication for an HA replica, and logical replication for CDC pipelines feeding other systems.

Advertisement

Replication slots and the slot-lag hazard

A replication slot is a bookmark in the WAL. When a subscriber connects and begins consuming changes, a slot is created with a restart_lsn (the WAL position from which the subscriber can resume). The database never discards WAL up to that position, ensuring no changes are lost. If the subscriber disconnects and reconnects within the retention window, it resumes from its last confirmed position. If it disconnects for longer than the retention window (or the slot is left unused), the WAL wraps around, but the slot still prevents discard—and WAL starts accumulating.

This is the slot-lag hazard: a subscriber that is slow, down for maintenance, or crashed will cause the source database to retain WAL indefinitely. On a busy database, WAL can grow rapidly (tens of GB per hour), and an unmanaged slot can fill the source's disk in hours, bringing the database to a halt. The solution is monitoring: query pg_replication_slots to check slot lag, alerting if any slot is far behind, and either fixing the consumer (restarting the subscriber, scaling Debezium, etc.) or dropping the slot if it is truly abandoned.

SELECT slot_name, restart_lsn, restart_lsn_age FROM pg_replication_slots;

The restart_lsn_age column shows how old the WAL being retained is. If it grows over time, the subscriber is falling behind. Set up monitoring (Prometheus, New Relic, DataDog) to watch slot lag and alert on thresholds (e.g., >10 GB).

Advertisement

Change data capture (CDC) with Debezium

Logical replication is the foundation for CDC, but it requires an application to consume the subscription and apply changes. Debezium is the popular open-source CDC platform that automates this: it uses logical decoding to capture changes and streams them to Kafka, where downstream consumers (analytics, search indexes, notification systems) pull changes and act on them.

The Debezium PostgreSQL connector connects to a publication on a source database, snapshots the tables (storing the current state in a snapshot topic), then streams incremental changes. Each row change becomes a Kafka message with the before-image, after-image, source metadata (database, table, transaction ID), and operation type (insert, update, delete). Downstream consumers subscribe to topics and react: updating a search index when a product changes, triggering notifications, feeding a data warehouse, or synchronizing a cache.

Debezium abstracts the details of logical replication (slot management, WAL retention, offset tracking) and handles schema evolution (propagating DDL changes downstream, versioning event schemas). For production CDC pipelines, Debezium is the standard choice—it is battle-tested, actively maintained, and integrates with Kafka, Confluent Cloud, and other platforms.

Practical use cases: migration, disaster recovery, and ETL

Cross-version database migration is a classic use case. To migrate from PostgreSQL 12 to 16 without downtime, set up logical replication from the old instance to the new one in parallel. The new instance receives a snapshot of all data, then continues receiving incremental changes as they occur on the old instance. Once the new instance is caught up and validated, cut over: redirect application traffic to the new instance. Logical replication handles the version difference seamlessly (schema differences are rare, but logical replication can tolerate minor structural changes because it operates at the row level, not the WAL level).

Selective disaster recovery: replicate only critical tables (e.g., users, orders, payments) to a backup database in another region, using publications to filter out less critical tables (logs, temporary data, etc.). This reduces RPO (recovery point objective) for critical data and RTO (recovery time objective) by keeping a warm standby without the overhead of replicating the entire database.

Analytics and ETL: stream operational data into an analytics database or data warehouse in real time. A subscription on an analytics PostgreSQL database (or Debezium pushing to Snowflake, BigQuery, etc.) keeps the warehouse synchronized with production data, eliminating stale batch jobs and enabling real-time dashboards and reports.

Event-driven microservices: use Debezium to stream database changes to a Kafka topic, where services subscribe and react. A new user signup triggers a welcome email service, a payment completion triggers a notification, an order change triggers inventory updates—all driven by database events, decoupling services from a single database and enabling eventual consistency across a microservices architecture.

Configuration best practices and avoiding pitfalls

Always enable wal_level = logical at initialization: changing it later requires a database restart and WAL truncation.

Use publication row and column filters sparingly: they reduce WAL overhead on the subscriber but not on the source (the source still decodes the full row). If you need to replicate only 10% of rows, consider a separate publication for that subset or filter at the subscriber level.

Monitor replication lag closely: use pg_stat_replication or pg_replication_slots to track write_lsn, apply_lsn, and slot lag. Set up alerts for lag > 1 minute or slot size > 5 GB.

Plan for schema changes: adding a non-nullable column without a default will break logical replication on the subscriber. Add columns with defaults, or drop and re-add the column with a default. For major schema changes, suspend the subscription, update schema on both sides, then resume.

Subscription slots are single-threaded: replication from a single subscription applies changes serially. For very large tables, parallelize by creating multiple subscriptions with different row filters (e.g., id ranges).

Snapshot overhead: the initial snapshot copies all data from the source. For large tables (100s of GB), the snapshot can take hours and consume significant bandwidth. Run snapshots during low-traffic windows or skip snapshots if the subscriber already has a copy of the data.

Monitoring, troubleshooting, and performance considerations

Lag monitoring: the key metric is the gap between the source's current WAL position (returned by pg_current_wal_lsn()) and the subscription's applied position. If lag is growing, the subscriber is falling behind. Causes are slow network, slow application of changes on the subscriber (locks, constraint violations, missing indexes), or the subscriber is simply down. Scale the subscriber, add indexes, or fix locks.

Constraint violations: if the subscriber's schema is slightly different (unique constraint added, foreign key added), applying changes may fail, and the subscription will stop. Use SELECT subscription_name, subskiplserror FROM pg_subscription to check if it is skipping LSN errors, or manually investigate pg_replication_origin_status.

CPU overhead on the source: logical decoding consumes CPU to decode the WAL. For high-throughput databases (millions of changes per minute), decoding can add 5–20% CPU overhead. Distribute the load by using multiple replication slots (multiple Debezium connectors pulling from the same publication in parallel) if needed.

Replica identity and FULL tracking: by default, UPDATE and DELETE events include the old row's primary key only. To include the full before-image (useful for audit logs or search index updates), set REPLICA IDENTITY FULL on the table. This adds overhead (more WAL, more decoding).

Limitations and when to avoid logical replication

Selective: not a standby. Logical replication is not a replacement for physical streaming replication for high-availability failover. It is higher-latency, requires more CPU, and is designed for one-way downstream synchronization, not bidirectional failover. For HA, use physical replication or a tool like Patroni.

Large transactions are slow: if your source commits a transaction that changes 10 million rows, logical decoding must decode all 10 million changes and stream them, blocking the source's checkpoint. This can cause temporary stalls. Avoid huge bulk operations or break them into smaller transactions.

Schema changes are complex: DDL changes (adding columns, renaming tables, dropping indexes) don't flow through logical replication—they don't produce row changes. Schema changes must be applied manually on both sides in sync, and coordination is required. Schema evolution in Debezium mitigates this, but it is still a coordination concern.

Not free: logical replication adds disk I/O (retaining WAL), CPU (decoding), and network (streaming changes). It is significantly cheaper than triggers or polling, but it is not free. For low-volume data, polling might be simpler; for very high volume, you may need to tune or shard.

Comparison with alternatives: Debezium, triggers, and polling

Debezium directly (without logical replication): Debezium includes a PostgreSQL connector that uses logical replication under the hood, but you need not run a separate PostgreSQL subscriber. Debezium connects to the source, manages the replication slot, and streams changes directly to Kafka. For most CDC use cases, Debezium is simpler: it handles offset management, exactly-once delivery, and schema versioning. Logical replication as a subscription is more useful when the target is another PostgreSQL database that needs a full replica.

Triggers: you can write a trigger on every insert/update/delete to push events to a queue or log table. Triggers are immediate but add latency and CPU overhead to the write path (every write fires the trigger), couple the application to the replication logic, and are harder to test and debug. Logical replication reads the WAL after the fact, so it has no write-path overhead and decouples replication from the application.

Polling: periodically query 'SELECT * FROM table WHERE updated_at > last_check' to find changes. Simple to implement but introduces lag (you miss changes between polls), inefficiency (you query the same rows repeatedly), and missed changes on failure (if the polling job crashes between polls). Logical replication captures every committed change in order without lag or inefficiency.

The hierarchy: for any production CDC or synchronization need, logical replication is the right foundation. Use Debezium for Kafka/multi-target streaming, or PostgreSQL subscriptions for database-to-database sync. Avoid triggers and polling.

PostgreSQL logical replication decodes the WAL into logical row-level changes and streams them to subscribers—specific tables to a different version, to Kafka via Debezium, or to a downstream application. It is the correct foundation for CDC, cross-version migration, and selective synchronization: no write-path overhead (unlike triggers), no lag or missed changes (unlike polling), and no version coupling (unlike physical replication). The critical operational concern is slot lag—a slow subscriber prevents WAL from being discarded and can fill the source's disk. Monitor slot lag closely, plan for schema changes, and use Debezium for production CDC pipelines. Logical replication is not a standby for high availability; it is a building block for event-driven, multi-target database synchronization.