Logical vs physical replication, replication slots and the slot bloat disaster, publication/subscription model, WAL sender process, Debezium + Kafka change data capture, zero-downtime major version upgrades using logical replication as a migration bridge, and replication identity modes.
Module 17 — Logical Replication and CDC Pipelines
We needed to upgrade from PostgreSQL 12 to PostgreSQL 16. Physical streaming replication doesn't cross major versions. The naive approach: pg_dump, restore, brief downtime. Our database was 8TB. The dump alone took 4 hours. The restore took 6 hours. Then indexing. Total downtime: 14 hours. We told the team we could do it in 2 hours. We were wrong. Logical replication, done correctly, would have given us 0 seconds of downtime.
Physical Replication vs Logical Replication
| Physical (Streaming) | Logical | |
|---|---|---|
| Unit of transfer | WAL bytes (block-level) | Logical row changes (decoded) |
| Cross-version support | Same major version only | Works across major versions |
| Selective tables | Entire cluster | Per-table granularity |
| DDL replicated automatically | Via WAL blocks only | Not automatically |
| Bi-directional | No | Yes, carefully |
| Primary use case | HA, hot standby, read replicas | CDC, version upgrades, ETL, Kafka |
Physical replication copies WAL bytes verbatim — the standby replays the exact same block writes. It cannot cross major versions because the on-disk format changes between them. Logical replication decodes the WAL into row-level changes (INSERT/UPDATE/DELETE), which are version-agnostic. That version-agnosticism is the property that makes the zero-downtime upgrade possible.
Replication Slots — The Critical Operational Detail
Replication slots are PostgreSQL's mechanism for ensuring the WAL needed by a subscriber is not discarded before the subscriber reads it. This sounds helpful. It is helpful until the subscriber goes away.
The disaster scenario: You create a logical replication slot. The subscriber is a Debezium connector that reads changes and publishes to Kafka. The Debezium Kafka Connect worker goes down for a maintenance window. Three days pass. PostgreSQL has been faithfully retaining WAL since the slot was last consumed. WAL accumulates on disk. Your 500GB disk fills up. PostgreSQL cannot write new WAL. PostgreSQL stops accepting writes. Your entire application is down. Every application team is paging the on-call. This scenario is not hypothetical — it has killed production systems.
Monitoring slot lag — make this a production alert, not an afterthought:
Alert when any slot's lag_size exceeds 5GB. Page someone when it exceeds 20GB.
Cap WAL retention per slot using max_slot_wal_keep_size (PostgreSQL 13+):
When a slot is dropped due to the size limit, the slot's active column goes to false and pg_replication_slots shows the slot as invalidated. The subscriber gets an error on its next attempt to read, knows it needs to resync, and can do so. Your disk stays healthy. Set the alert lower than the limit so you have time to act before the slot is dropped.
Publication and Subscription
On the source database:
On the destination database:
PostgreSQL handles the initial data sync (copying existing rows) and then streams ongoing changes. No application changes required during initial sync.
Monitoring subscription progress:
Replication Identity — The Row Identification Problem
Logical replication needs to identify which row to UPDATE or DELETE on the destination. Without a way to find the target row, UPDATE and DELETE cannot be applied.
Tables without a primary key and without REPLICA IDENTITY FULL will fail to replicate UPDATEs and DELETEs:
Before setting up logical replication, audit all tables in the publication for missing primary keys:
Zero-Downtime Major Version Upgrade
The playbook for upgrading PostgreSQL 12 to PostgreSQL 16 with under 5 minutes of write downtime.
Step 1: Prepare the source (PG12)
Step 2: Copy the schema to PG16
DDL must exist on the destination before you create the subscription — otherwise the initial sync fails when it tries to copy data into tables that don't exist.
Step 3: Create the subscription on PG16
PostgreSQL begins copying existing rows immediately. For an 8TB database, this takes hours. The source database continues serving traffic normally. All changes made during the sync are captured and will be applied after the initial copy completes.
Step 4: Monitor lag until near-zero
When lag is below 1MB, you are ready for the cutover window.
Step 5: The cutover (aim for under 5 minutes)
The application-visible downtime is steps 1 through 6 — typically 1–3 minutes if you have done the preparation. The 14-hour downtime from the pg_dump approach is gone.
Debezium + Kafka CDC Architecture
Debezium reads PostgreSQL's logical replication stream and publishes row-level changes to Kafka topics. Every INSERT, UPDATE, DELETE becomes a Kafka message. Downstream services consume changes without polling the database.
op values: c (create/insert), u (update), d (delete), r (read during snapshot).
Topic design
Compacted topics retain only the latest message per key. Good for reference data (products, users) where you care about the current state, not the full change history. Non-compacted topics retain every change — necessary for event data (orders, transactions) where each change is meaningful.
Debezium connector configuration
The heartbeat is critical. If your monitored tables have no changes for hours, the replication slot's restart_lsn does not advance — because there's nothing to consume. The slot lag metric grows even though the connector is healthy and connected. The heartbeat writes a small change to a dedicated heartbeat table on a regular interval, advancing the LSN and keeping the lag metric accurate. Without it, your slot lag alert fires false positives constantly.
DDL Changes — The Logical Replication Blindspot
Logical replication does NOT replicate DDL. If you run ALTER TABLE orders ADD COLUMN shipped_at timestamptz on the source, the column does not appear on the destination. Ongoing INSERTs that include shipped_at will fail on the destination because the column doesn't exist there.
ERROR: column "shipped_at" of relation "orders" does not exist
The subscription stops. Debezium stops consuming. Your replication slot lag starts growing.
For the major version upgrade pattern
Apply all DDL to PG16 before enabling the subscription. The pg_dump --schema-only step handles the initial DDL. Schema changes after that point must be applied to PG16 manually before they are applied to PG12.
The workflow during the upgrade period:
For Debezium CDC
Schema changes require a controlled process:
Some CDC platforms (Confluent Schema Registry, Fivetran) handle schema evolution automatically. If you are managing Debezium manually, this coordination step is yours to own. Missing it means 3am pages from a stopped connector and growing slot lag.
Bi-Directional Replication — The Careful Part
Logical replication supports bi-directional setups (both databases can write, changes replicate in both directions). This enables active-active architectures, blue-green database deployments, and merging databases.
The catch: bi-directional replication requires conflict resolution. If two clients write to the same row on different nodes simultaneously, both changes get replicated to the other node and one of them will fail.
PostgreSQL's logical replication has no built-in conflict resolution. You must:
- Design your application to avoid write conflicts (partition writes by region, user, or shard)
- Or use a third-party tool that adds conflict resolution (BDR from EDB, Citus, etc.)
For the major version upgrade case, bi-directional is unnecessary — you are replicating in one direction until cutover, then switching completely. Don't add bi-directional complexity unless you have a specific operational reason.
The Replication Slot Disaster, Revisited
The slot lag disaster from Module 4's production incident is worth examining from the logical replication angle. The setup:
- PostgreSQL with a logical replication slot for a Debezium connector
- Debezium goes offline for 3 days (Kafka Connect cluster maintenance)
- No
max_slot_wal_keep_sizeconfigured - No slot lag alerting
What the monitoring dashboard should have shown:
What the recovery looked like:
Debezium resyncs by running a new snapshot (full table scan of all configured tables) and replaying from there. Depending on table sizes, this takes minutes to hours. During resync, downstream Kafka topics may have duplicate messages if Debezium uses at-least-once delivery — your consumers should be idempotent.
Production Checklist
Before enabling logical replication for CDC or a version upgrade:
Logical replication is not plug-and-play. The initial setup is straightforward. The operational discipline — slot monitoring, DDL coordination, sequence management — is where production incidents happen.
Summary
| Concept | Key Takeaway |
|---|---|
| Physical vs. logical | Physical copies WAL bytes (same major version only). Logical decodes row changes (cross-version). |
| Replication slots | Retain WAL for subscribers. Inactive slots cause disk-filling disasters. Monitor lag. |
max_slot_wal_keep_size | Cap WAL per slot. Slot gets dropped if it falls behind — subscriber resyncs, disk stays healthy. |
| Publication/subscription | Source defines what to publish. Destination subscribes and receives initial sync + ongoing changes. |
| Replica identity | Tables need a PK (or REPLICA IDENTITY FULL) for UPDATE/DELETE to replicate correctly. |
| Major version upgrade | Logical replication to new version + near-zero lag + brief maintenance window = minutes of downtime, not hours. |
| Debezium/CDC | Reads the replication stream, publishes to Kafka. Heartbeat keeps slot lag metric accurate. |
| DDL | Not replicated. Must be applied manually to destination before being applied to source. |
| Bi-directional | Possible but requires conflict resolution strategy. Avoid unless you have a specific requirement. |
Next: P-10 — Zero-Downtime Schema Migrations →
A critical production PostgreSQL instance is configured with a logical replication slot for a Debezium connector. Due to an unforeseen issue, the Debezium Kafka Connect worker goes offline for 48 hours. The PostgreSQL instance does *not* have max_slot_wal_keep_size configured. Which of the following is the most likely and severe outcome, and what is the primary architectural mitigation to prevent it?
A development team is performing a schema migration, adding a new non-nullable column with a default value to an orders table that is actively being logically replicated from a PostgreSQL 12 source to a PostgreSQL 16 destination as part of a zero-downtime major version upgrade. The team applies the ALTER TABLE statement to the PostgreSQL 12 source database first, then immediately attempts to apply it to the PostgreSQL 16 destination. What is the most likely immediate consequence, and what is the correct sequence of operations to prevent it?
A Senior SRE has configured a Debezium connector to stream changes from a PostgreSQL database to Kafka using logical replication. They have set up robust monitoring for replication slot lag using pg_replication_slots, with alerts configured to page on-call engineers if lag_size exceeds 5GB. However, they neglected to configure the Debezium heartbeat mechanism. What is the most likely operational challenge this oversight will cause, and why?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
Sign in & RegisterDiscussion
0Join the discussion