Change data capture is where a database stops being a datastore and starts being an event source. Most teams meet it through Debezium: point a connector at the database, get a Kafka topic per table, build the downstream world on that stream. From the consumer side, a MySQL-sourced pipeline and a PostgreSQL-sourced pipeline look nearly identical. From the operator side, they are different machines with different failure modes, and a team migrating from MySQL to PostgreSQL should not assume their CDC runbook carries over.
I have operated both kinds of pipeline. This article compares how each engine exposes changes, how initial snapshots work, what happens when someone runs a DDL migration mid-stream, and which lag and retention signals deserve alerts.
MySQL CDC: pretending to be a replica
MySQL has no dedicated change-capture API, and it turns out not to need one, because the binlog already is one. A Debezium MySQL connector connects using the replication protocol, presents itself with a server_id like any replica would, and reads row events from the binlog stream. For that to produce usable change events, the server must run binlog_format=ROW, and you want binlog_row_image=FULL so update and delete events carry complete before-images rather than just changed columns. GTID mode is strongly recommended so the connector's position survives topology changes.
The elegance of this design is that CDC inherits two decades of replication hardening for free. The cost is that CDC also inherits replication's retention model: the binlog is a shared, time-expiring resource. The server does not know or care that a CDC connector exists. If the connector is down for longer than binlog retention (binlog_expire_logs_seconds), the events are simply gone, the stored offset points into purged history, and the connector must re-snapshot. Every experienced MySQL CDC operator has either lived through that or tuned retention generously to avoid it, trading disk for safety margin.
PostgreSQL CDC: logical decoding and publications
PostgreSQL took the opposite route and built a first-class API. With wal_level=logical, the server can decode its physical WAL back into logical row changes and hand them to an output plugin through a replication slot. The built-in plugin, pgoutput, is the same one native logical replication uses, and Debezium's PostgreSQL connector speaks it directly, so no third-party plugin installation is needed on modern versions. What gets streamed is scoped by a publication, a catalog object naming the tables (and optionally the operations) to publish.
-- On the source database
ALTER SYSTEM SET wal_level = 'logical'; -- restart required
CREATE PUBLICATION cdc_orders
FOR TABLE public.orders, public.order_items
WITH (publish = 'insert, update, delete');
-- Updates and deletes need identity; FULL if no suitable key
ALTER TABLE public.order_items REPLICA IDENTITY FULL;
REPLICA IDENTITY deserves the callout because it is the step people miss. For a table with a primary key, the default identity is fine and update or delete events carry the key columns. For tables without one, you either set REPLICA IDENTITY FULL, which writes entire old rows into WAL and inflates WAL volume on update-heavy tables, or you accept that deletes cannot be usefully decoded. MySQL's row format with full row image gives you complete before-images everywhere by default; PostgreSQL makes you opt in per table and pay for it in WAL bytes. That is a genuine point in MySQL's favor for wide-table CDC.
Retention, meanwhile, flips from MySQL's weakness to PostgreSQL's promise. The replication slot pins WAL until the connector confirms it, so a connector outage never loses events to a retention timer. The flip side is the classic slot hazard: a stopped connector means WAL accumulates on the primary until the disk fills or max_slot_wal_keep_size invalidates the slot. MySQL CDC fails toward silent data loss and a forced re-snapshot; PostgreSQL CDC fails toward a visible, alertable disk problem on the source. I prefer the failure you can see coming, but only if you actually watch for it.
Snapshots: getting to a consistent starting point
Both connectors face the same bootstrap problem: the change stream is useless without the state it applies to. Debezium's MySQL connector takes an initial snapshot by reading table contents at a consistent point, recording the binlog position that corresponds to it, and then streaming from that position. Getting consistency historically involved global read locks or, with the right privileges, a brief lock plus a REPEATABLE READ transaction; on large databases, teams often prefer incremental snapshots, which chunk through tables while streaming continues and deduplicate via watermarking.
PostgreSQL gives the connector a cleaner primitive. Creating a logical replication slot can export a snapshot: a frozen, consistent view of the database exactly at the slot's starting LSN. The connector reads the initial data through that exported snapshot in an ordinary transaction, no global locks, and starts decoding from the same LSN with zero gap and zero overlap. Native logical replication does the same trick with its initial table copy, which the logical replication guide covers in more depth. Incremental and ad-hoc snapshots exist on the PostgreSQL connector too, but the exported-snapshot path makes the common case notably less invasive than on MySQL.
Schema changes: the part nobody enjoys
DDL is where both pipelines get interesting, in different ways. The MySQL binlog contains DDL statements, so Debezium parses them as they stream past and maintains a schema history topic that lets it interpret row events with the table definition that was current when each event was written. This is impressive machinery, and it mostly just works, but it means the connector carries a full MySQL DDL parser, and exotic statements or out-of-band history topic damage can wedge a connector in ways that take real effort to repair.
PostgreSQL logical decoding does not emit DDL at all. The WAL contains the schema change physically, but pgoutput does not publish it, so your pipeline never sees an event saying a column was added. Decoding always uses current catalog information for each change, so the stream stays self-consistent, but downstream consumers must learn about schema evolution some other way: coordinated deploys, schema registries, or tolerant consumers that accept new fields. Additive changes (new nullable columns, new tables added to the publication) are routine; destructive ones (drops, type rewrites) need the same choreography you would use for any contract change between services. Neither model is free. MySQL automates schema tracking and occasionally breaks mysteriously; PostgreSQL refuses to automate it and makes the coordination your explicit job. After running both, I have come to prefer explicit, but I understand teams who feel otherwise.
Lag and retention: what to alert on
On the MySQL side, the vital signs are the connector's binlog position versus the server's current position, connector task state, and time-to-retention-horizon: how long the connector could stay down before binlog expiry strands it. On the PostgreSQL side, the equivalent signals live in the database itself, which is convenient because your database monitoring can own them.
SELECT s.slot_name,
s.active,
s.wal_status,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), s.confirmed_flush_lsn)
) AS decode_lag,
st.spill_txns,
st.spill_bytes
FROM pg_replication_slots s
LEFT JOIN pg_stat_replication_slots st USING (slot_name)
WHERE s.slot_type = 'logical';
The confirmed_flush_lsn gap is your end-to-end decode lag in bytes. pg_stat_replication_slots (PostgreSQL 14+) adds spill statistics: transactions too large for logical_decoding_work_mem spill to disk during decoding, and a workload full of giant transactions will show up there long before consumers complain. Watch for one more PostgreSQL-specific trap: a long-running open transaction on the source stalls decoding progress for everything behind it, so slot lag alerts often catch application transaction leaks that nothing else surfaced. General guidance on wiring these signals into a baseline lives in PostgreSQL monitoring.
Which pipeline would I rather operate?
MySQL CDC is a mature, pragmatic piggyback on replication: easy to start, hardened by ubiquity, with retention as its structural weak point and DDL parsing as its occasional sharp edge. PostgreSQL CDC is a designed capability: cleaner snapshots, precise slot-based retention, and honest refusal to guess at schema evolution, with slot disk risk and REPLICA IDENTITY as the tuition you pay. Migrating teams should budget real time for two things: rewriting retention alerting around slots instead of expiry timers, and moving schema-change handling from "the connector figures it out" to an explicit process. Do those two and the PostgreSQL pipeline is, in my experience, the quieter one to be on call for. The broader platform tradeoffs are covered on the PostgreSQL page.
How MonPG helps once you are on PostgreSQL
MonPG monitors PostgreSQL only, and CDC is one of the workloads where that focus pays off. It tracks logical slot lag and retained WAL per slot with history, so a connector that stopped confirming at 03:00 is a timeline, not a mystery; it alerts on inactive slots and retention growth before max_slot_wal_keep_size has to intervene; and it correlates WAL generation rate with query activity, so you can tell a slow consumer from a suddenly chatty producer. Long-transaction visibility rounds it out, since stalled decoding so often traces back to one forgotten open transaction. If your CDC pipeline is about to stand on PostgreSQL, PostgreSQL monitoring shows the baseline worth having before the first incident.