/

Data Infrastructure

MySQL CDC: Binlog Replication Explained

Intergalactic Data Labs

โ€”

โ€”

12 min read

Table of contents

Summarize

MySQL change data capture (CDC) reads the server's binary log the way a replica does and turns each committed insert, update, and delete into an event applied elsewhere. As of October 2026 there are three ways to run it. Use a Kafka Connect stack such as Debezium, a managed service such as Fivetran or ClickPipes, or a single-process reader such as Filament, Maxwell, or Sling.

By the end you will have a MySQL table streaming into Postgres with deletes applied, a checkpoint that survives restarts, and a scheduled catch-up run.

Need

Value

Where it is set

MySQL version

8.0 or later

Source server

Binlog format

ROW with full row image

my.cnf or parameter group

Binlog retention

Longer than your longest pause

binlog_expire_logs_seconds or vendor setting

Privileges

REPLICATION SLAVE, REPLICATION CLIENT, SELECT, LOCK TABLES

GRANT on the CDC user

Connector maturity

MySQL source beta, Postgres sink beta

Filament docs

Time to complete

About 30 minutes on a clean server

Your terminal

How MySQL binlog replication works

A CDC reader is a fake replica. It registers with the server, asks for a binlog dump from a position, and decodes the row events that follow. The 8.4 reference manual says the binary log "contains 'events' that describe database changes such as table creation operations or changes to table data."

The reader never queries your tables after the first snapshot, which is why CDC beats polling an updated_at column. A timestamp cursor "cannot see a hard delete", as the replication modes page puts it, while the binlog carries the deleted row.

Four settings decide whether the stream works at all, and two more decide how a reader names columns and identifies itself. ROW has been the default format since MySQL 5.7.7, and the binlog is on by default in 8.0 and later.

Setting

What CDC needs

Default in MySQL 8.4

binlog_format

ROW

ROW, deprecated since 8.0.34

binlog_row_image

full

full

binlog_row_metadata

MINIMAL works for Filament, FULL for most tools

MINIMAL

binlog_expire_logs_seconds

Cover the longest outage

2592000, 30 days

gtid_mode

ON preferred

OFF

server_id

Unique per reader

Set by the tool

Defaults come from the binary log options page and the GTID options page. With minimal row image the before image of an update carries only the primary key, so a merge sink gets partial rows. Column names are not in the binlog, so a reader needs FULL metadata or looks names up in information_schema.

The checkpoint is the other half. A GTID set survives failover to a promoted replica, per the GTID concepts page, while a file and position checkpoint breaks when the file sequence changes.

How to set up MySQL CDC step by step

Galaxy builds Filament, the worked example below. Its MySQL source is beta, per the connector page, and the whole flow runs from one binary with checkpoints in local SQLite. No public benchmark covers a MySQL CDC route, so this guide makes no speed claim.

Step 1. Check the binlog settings on the source

Run the checks every CDC vendor asks for before touching anything else.

SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';
SHOW VARIABLES LIKE 'binlog_row_image';
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';
SHOW VARIABLES LIKE 'gtid_mode'

SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';
SHOW VARIABLES LIKE 'binlog_row_image';
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';
SHOW VARIABLES LIKE 'gtid_mode'

SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';
SHOW VARIABLES LIKE 'binlog_row_image';
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';
SHOW VARIABLES LIKE 'gtid_mode'

You want ON, ROW, FULL, retention that covers a weekend, and ideally ON for GTID.

Step 2. Fix format and retention where you run MySQL

On a self-managed server set the values in my.cnf and restart. Retention is the setting most teams get wrong, so size it for the longest time the reader could be down.

[mysqld]
binlog_format = ROW
binlog_row_image = FULL
binlog_expire_logs_seconds = 604800
gtid_mode = ON
enforce_gtid_consistency = ON
[mysqld]
binlog_format = ROW
binlog_row_image = FULL
binlog_expire_logs_seconds = 604800
gtid_mode = ON
enforce_gtid_consistency = ON
[mysqld]
binlog_format = ROW
binlog_row_image = FULL
binlog_expire_logs_seconds = 604800
gtid_mode = ON
enforce_gtid_consistency = ON

Managed services move these knobs. On RDS for MySQL, automated backups control whether the binlog exists at all and binlog_format lives in a custom parameter group, per the RDS binlog page. Retention is a stored procedure in the RDS reference.

CALL mysql.rds_set_configuration('binlog retention hours', 168);
CALL

CALL mysql.rds_set_configuration('binlog retention hours', 168);
CALL

CALL mysql.rds_set_configuration('binlog retention hours', 168);
CALL

Service

Binlog on

Format

Retention setting

Self-managed 8.x

Default ON

my.cnf

binlog_expire_logs_seconds

RDS for MySQL

Automated backups on

Parameter group

binlog retention hours, max 168

Aurora MySQL

Cluster parameter group, default OFF

Cluster parameter group, reboot

binlog retention hours, max 2160

Cloud SQL

Point-in-time recovery

Managed, always ROW

Transaction log days, 1 to 35

Azure Flexible Server

Always ON

Always ROW

binlog_expire_logs_seconds

PlanetScale

No raw binlog

Vitess VStream

Use the PlanetScale Debezium connector

Sources are Aurora, Cloud SQL PITR and flags, Azure, and PlanetScale. Azure's default retention of zero deletes a binlog as soon as its handle is freed, so set it.

Step 3. Create the replication user

The reader needs both replication privileges plus SELECT and LOCK TABLES on the tables it bootstraps. The manual's replication user example is the model.

CREATE USER 'filament'@'%' IDENTIFIED BY 'change-me';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'filament'@'%';
GRANT SELECT, LOCK TABLES ON shop.* TO 'filament'@'%'

CREATE USER 'filament'@'%' IDENTIFIED BY 'change-me';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'filament'@'%';
GRANT SELECT, LOCK TABLES ON shop.* TO 'filament'@'%'

CREATE USER 'filament'@'%' IDENTIFIED BY 'change-me';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'filament'@'%';
GRANT SELECT, LOCK TABLES ON shop.* TO 'filament'@'%'

MySQL 8.4 defaults new accounts to caching_sha2_password, which needs TLS or RSA key exchange, per the authentication plugin docs. Use TLS.

Step 4. Install the reader

One command installs the CLI, per the installation page. The local CLI uses file-backed SQLite, so there is no Postgres or NATS to stand up first.

curl -fsSL https://getgalaxy.io/filament/install | sh

curl -fsSL https://getgalaxy.io/filament/install | sh

curl -fsSL https://getgalaxy.io/filament/install | sh

Step 5. Register the MySQL source in CDC mode

The CLI reference documents the flag rule, where connector fields become flags with a source- prefix and underscores become hyphens. The docs show it with a Postgres example, so the MySQL flags below are derived from the connector's key table. Confirm them with filament source create --help, which lists the selected connector's fields.

export MYSQL_DSN='mysql://filament:change-me@db.internal:3306/shop?tls=true'
filament source create shop \
  --source-connector mysql \
  --source-connection-method url \
  --source-dsn-env MYSQL_DSN \
  --source-replication cdc
filament source

export MYSQL_DSN='mysql://filament:change-me@db.internal:3306/shop?tls=true'
filament source create shop \
  --source-connector mysql \
  --source-connection-method url \
  --source-dsn-env MYSQL_DSN \
  --source-replication cdc
filament source

export MYSQL_DSN='mysql://filament:change-me@db.internal:3306/shop?tls=true'
filament source create shop \
  --source-connector mysql \
  --source-connection-method url \
  --source-dsn-env MYSQL_DSN \
  --source-replication cdc
filament source

The replication field switches the connection from query reads to binlog streaming. Discovery prints resource names, primary keys, and estimated row counts, so you see which tables have the keys a merge needs.

Step 6. Register the Postgres sink

This is the documented sink command, since the CLI reference example is already a Postgres sink.

export POSTGRES_SINK_DSN='postgres://filament:change-me@warehouse.internal:5432/analytics'
filament sink create analytics \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env

export POSTGRES_SINK_DSN='postgres://filament:change-me@warehouse.internal:5432/analytics'
filament sink create analytics \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env

export POSTGRES_SINK_DSN='postgres://filament:change-me@warehouse.internal:5432/analytics'
filament sink create analytics \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env

The sink page advertises cdc_append and cdc_merge. Merge "maintains current state" by upserting inserts and updates and deleting on deletes, while append keeps every change as a history row.

Step 7. Create the CDC pipeline with merge writes

CDC applies to the whole route, so every resource in the pipeline streams from the binlog.

filament pipeline create shop-cdc \
  --source shop \
  --sink analytics \
  --resources orders,customers \
  --sync-mode cdc \
  --write-mode

filament pipeline create shop-cdc \
  --source shop \
  --sink analytics \
  --resources orders,customers \
  --sync-mode cdc \
  --write-mode

filament pipeline create shop-cdc \
  --source shop \
  --sink analytics \
  --resources orders,customers \
  --sync-mode cdc \
  --write-mode

Merge is the right default for one documented reason. The connector page says "A crash during a transaction replays the whole transaction, so use merge writes that converge by primary key."

Step 8. Run it once, then schedule the catch-up

The first run bootstraps each table. The reader captures the binlog position, holds read locks for the instant it takes to open a consistent snapshot, copies the table, then streams from that position.

filament run shop-cdc
filament run ls

filament run shop-cdc
filament run ls

filament run shop-cdc
filament run ls

Every later run is a catch-up. It captures the current binlog position as a watermark, streams from the checkpoint up to it, and returns, so continuous CDC "can run on a schedule". Put filament run shop-cdc on a cron, or deploy the server and attach a schedule through the pipelines API.

How to verify MySQL CDC worked

Check the position on both sides, then prove a delete lands. On MySQL 8.4 the position statement was renamed, per the 8.4 changes list, so use the new form.

SHOW BINARY LOG STATUS\G
-- MySQL 8.0 uses SHOW MASTER STATUS\G
SHOW BINARY LOG STATUS\G
-- MySQL 8.0 uses SHOW MASTER STATUS\G
SHOW BINARY LOG STATUS\G
-- MySQL 8.0 uses SHOW MASTER STATUS\G

Then run the real test. Delete one order in MySQL, run filament run shop-cdc, and confirm the row is gone from analytics.public.orders. Update a customer and confirm the new values replaced the old ones rather than appearing as a second row.

A run is done only when its state is completed, which the runs and recovery page defines as the sink having committed. CDC saves its position only after that commit, so a failed attempt never advances the checkpoint.

What to do when MySQL CDC breaks

Five failures account for most MySQL CDC outages, and each has one fix. Codes and strings come from the 8.4 server error reference and the binlog sender source in the MySQL server repo.

Error

Code

Cause

Fix

Source purged required binary logs

1789

Retention shorter than the pause

Re-snapshot, raise retention

Could not find first log file name

1236

File position rotated away

Same as above, prefer GTID

A replica with the same server_id has connected

1236

Two readers share a server_id

Set a unique server_id per reader

Access denied, REPLICATION SLAVE privilege

1227

Missing grant

Re-run the GRANT from step 3

Got a packet bigger than max_allowed_packet

1153

Large row or blob event

Raise the limit on server and client

The purged-log pair is the one that bites in production. Both mean the reader paused longer than the binlog lived, and the only recovery is a fresh snapshot, which is why step 2 sized retention for a weekend.

The duplicate server_id error is subtle because the newer connection kicks the older one off, so a staging reader pointed at production breaks the real one. Filament's default server_id is 62347, so change it for a second reader.

The large packet error, on the packet too large page, needs the change on both ends because a binlog event is itself a packet. One Filament-specific case remains. After DDL that changes the column count, the run fails on purpose rather than mis-mapping values, so re-snapshot that table.

Other ways to run MySQL CDC and what they cost you

Every tool below reads the same binlog, so the differences are deployment, license, and billing unit. Licenses come from LICENSE files, not README badges.

Tool

Runs as

License

Billing unit

Documented limit on MySQL CDC

Debezium

Kafka Connect or Debezium Server

Apache 2.0

Free

Needs FULL row image and table locks on RDS

Airbyte

Platform, embeds Debezium

ELv2

Credits

Needs FULL row image, release stage alpha

Fivetran

SaaS

Proprietary

Monthly active rows

Binlog bypasses column grants

Estuary Flow

SaaS

BSL 1.1

Per GB plus per connector

Wants 7 days of retention

Maxwell

JVM daemon

Apache 2.0

Free

Writes to queues only, no warehouse merge

Sling

CLI

GPL 3.0

Pro token for CDC

Fails if binlog is purged

ingestr

CLI, Go

FSL 1.1

Free

Fails rather than partial snapshot on lost position

ClickPipes

ClickHouse Cloud

Proprietary

Per GB

Requires GTID and 72 hour retention

Filament

Single binary or Go library

Apache 2.0

Free

No CDC into its ClickHouse sink

Debezium is the reference implementation with eight snapshot modes in its MySQL connector docs, but you run a Kafka Connect cluster or a Debezium Server for it. Fivetran calls binlog "the most efficient mechanism for capturing change data" in its setup guide and warns that column permissions do not apply to binary logs.

Maxwell shipped a release in September 2026, but its producers are queues, so something else must apply the events. Sling makes each run bounded and safe to schedule, and gates CDC behind a paid tier on its change capture page.

ClickPipes went generally available on September 2, 2026 and fits ClickHouse Cloud destinations. dlt has no MySQL binlog source, so it is not in the table. Pick a Kafka tool when several consumers need the events, a managed service when nobody will run the reader, and a single-process reader when the destination is one database you already operate.

Next steps

Frequently asked questions

What is MySQL CDC?

MySQL change data capture reads the server's binary log as a replica would and turns each committed insert, update, and delete into an event a downstream system applies. It needs ROW format, enough binlog retention, and a user with the replication privileges. Unlike timestamp polling, it sees hard deletes and never rescans tables.

Is the MySQL binary log enabled by default?

Yes on MySQL 8.0 and later, where log_bin is ON unless the server was started with the initialize option. The default format is ROW and the default retention is 30 days. Managed services differ. RDS turns the binlog on only with automated backups enabled, and Aurora defaults binlog_format to OFF.

What is the best binlog format for MySQL CDC?

ROW. Statement and mixed logging record SQL text, which a change reader cannot turn into before and after row images, so every CDC tool rejects them. Pair ROW with binlog_row_image set to full so update and delete events carry every column a merge sink needs.

Can MySQL CDC capture deletes?

Yes, and that is the main reason to pick it over cursor-based incremental sync. A delete appears in the binlog as a row event with the old row image, so the reader emits a delete that a merge sink applies by primary key. An updated_at cursor never sees the row vanish.

Does MySQL CDC affect database performance?

Lightly. The server already writes the binlog for backups and replicas, so a CDC reader adds one more replica connection streaming events over TCP. The costs that do show up are disk for longer retention and the brief read locks some tools take while bootstrapping a table.

What happens when the binlog is purged before CDC reads it?

The stream cannot resume. MySQL returns error 1789 for a GTID checkpoint or a fatal error 1236 saying it could not find the first log file for a file position. The fix is a fresh snapshot of the affected tables plus longer retention through binlog_expire_logs_seconds or the vendor's setting.

Does Filament need a separate full load before MySQL CDC?

No. When a selected table has no CDC checkpoint, Filament reads it in full through a consistent snapshot and continues the stream from the binlog position captured just before that snapshot opened. The baseline and the stream share one point, so a pipeline starts on CDC alone.

Does Filament use GTIDs for MySQL CDC checkpoints?

When gtid_mode is ON it checkpoints the GTID set, which the docs call preferred because it survives replica promotion. With GTID off it stores the binlog file and position instead. An existing file position checkpoint keeps that format even if GTID is enabled later, so enable GTID before the first run.

More articles

Stay up to date with what weโ€™re building

Stay up to date with what weโ€™re building

Stay up to date with what weโ€™re building

Questions

Answered

FAQ

What does Galaxy do?

What is Filament?

What is enterprise context management?

What does working with Galaxy look like?

How do you handle security and compliance?

Why does Galaxy build in the open?

Own your knowledge stack

Own your knowledge stack

Copyright ยฉ 2026 Galaxy. All rights reserved.