/

Data Platforms

MySQL to Snowflake: The Complete Replication Guide

Intergalactic Data Labs

—

—

16 min read

Table of contents

Summarize

As of September 2026 there are three ways to replicate MySQL to Snowflake. Snowflake's own Openflow connector reads the binlog from Snowflake's managed runtime. Hosted services like Fivetran, Airbyte, and Estuary do it from the vendor's cloud on a meter. Self-hosted CDC tools like Debezium, Sling, ingestr, and Filament run inside your network. Three questions decide. Must deletes propagate, may rows leave your network, and how often may the warehouse wake.

Approach

Change capture

Deletes

Where it runs

Pricing unit

Snowflake Openflow

Binlog CDC

Soft-delete flag

Snowflake runtime or your AWS VPC

Snowflake credits

Hosted ELT

Binlog CDC or query polling

Flag or hard delete

Vendor cloud

Rows, GB, or events

Self-hosted CDC

Binlog CDC

Hard delete or flag

Your servers

Free or flat rate

How does MySQL to Snowflake replication work?

Every approach does the same two jobs. It copies the tables as they stand into Snowflake, then it keeps Snowflake current by reading the changes MySQL writes to its binary log.

The binlog is the change source. The MySQL 8.4 manual calls the binary log "a record of the data changes to be sent to replicas." It is on by default, and the server source sets row format as the default, which every log-based tool needs. A replication tool connects as if it were a replica and applies each insert, update, and delete downstream.

Change data capture, or CDC, is the practice of reading that log instead of re-querying tables. The alternative is an incremental read on a timestamp column, which never sees a deleted row. Airbyte's MySQL docs make the case for the log, since binlog replication lets it "incrementally capture deletes in addition to new and updated records."

Snowflake loads files, not rows. Snowflake's fast path is COPY INTO, which loads staged Parquet, CSV, JSON, Avro, or ORC files from an internal stage or an external bucket. Every tool on this route stages a batch, runs COPY INTO, and applies the result with a MERGE, which Snowflake documents as useful "if the second table is a change log."

Passwords are on their way out. Snowflake's MFA rollout says it will "disallow passwords for all service users," with strong authentication required for everyone between August and October 2026. That means a key pair of at least 2048 bits on a SERVICE user, whichever tool you choose.

What decides which MySQL to Snowflake tool to use?

Six criteria settle the choice. Rank them by which one would hurt most if it went wrong.

  • Deletes. A query-based sync silently keeps rows MySQL deleted. Only binlog CDC sees them, and the tool then removes the row in Snowflake or flags it.

  • Consistent start. The snapshot and the change stream must share one point in the binlog, or rows land twice or never.

  • Warehouse wake-ups. Snowflake bills compute per second with a one minute minimum, so a tool that loads every few seconds costs more than one that batches.

  • Where the data runs. A hosted service routes rows through the vendor's cloud. A self-hosted tool keeps them inside your network.

  • Auth. The tool must support key-pair or token auth for the Snowflake service user.

  • Binlog retention. The log must outlive the snapshot and any pause. MySQL defaults to 30 days, but on Amazon RDS the default "means binary logs aren't retained," per the RDS docs.

Fivetran states the warehouse point directly in its Snowflake destination docs. "Because Snowflake bills for compute usage on a per-second basis, they charge you when Fivetran loads data." Its advice is an X-Small warehouse with one-minute auto-suspend, and that advice applies to every tool below.

Which tools replicate MySQL to Snowflake?

Twelve tools run this route. The table lists how each captures changes, what it does with deletes, where it runs, its license as stated in its LICENSE file, and its pricing unit.

Tool

Change capture

Deletes

Deployment

License

Pricing unit

Filament

Binlog CDC

Merge removes

Binary, Go library, Helm

Apache 2.0

None

Snowflake Openflow

Binlog CDC

Flag

Snowflake runtime or AWS BYOC

Proprietary

Snowflake credits

Snowflake Connector for MySQL

Binlog CDC

Flag

Native app plus Docker agent

Proprietary

Snowflake credits

Fivetran

Binlog or Teleport

Flag

Hosted, Hybrid on Enterprise

Proprietary

Monthly active rows

Airbyte

Binlog via Debezium

Flag, hidden in final table

Cloud or Helm

ELv2

Credits per GB

Estuary Flow

Binlog CDC

Hard or flag

Hosted, private, BYOC

BSL 1.1

GB plus per connector

Debezium plus Kafka connector

Binlog to Kafka

Append only

Kafka Connect

Apache 2.0

Free, Snowpipe per GB

dlt

Query based

Not from MySQL

Python library

Apache 2.0

Free, dltHub paid

Sling

Binlog CDC, paid tier

Hard or flag

CLI

GPL-3.0

Flat rate token

ingestr

Binlog CDC

Flag

CLI

FSL-1.1

Free

Hevo

Binlog CDC

Flag

Hosted

Proprietary

Events per month

Stitch

Log based

Flag

Hosted

Proprietary

Rows per month

Snowflake Openflow is the native answer and it is generally available. Built on Apache NiFi, it runs a snapshot and then applies CDC through MERGE on a replication key. Deletes set a _SNOWFLAKE_DELETED column rather than removing the row. The docs list real limits. It "supports only username and password authentication with MySQL," it cannot read Aurora reader instances, and BYOC is AWS only.

The Snowflake Connector for MySQL is the older native app. Its own page says it stays in preview, that general availability "is currently not on our product roadmap," and that Openflow replaces it. Treat it as a migration target, not a new install.

Fivetran has the broadest connector catalog and a mature MySQL source that pulls a full dump and then connects to the binary log. Its Teleport Sync captures changes with only a read-only SQL user when the binlog is unavailable. Deletes arrive as a _fivetran_deleted flag, since "we don't delete rows from the destination." The meter is monthly active rows, which grows with your data.

Airbyte ships a certified MySQL source with binlog CDC through Debezium and checkpointed, chunked reads. Its Snowflake destination writes "each stream into its own raw table" plus a typed final table. Every table exists twice, and the raw copy "is not deduplicated." The LICENSE is Elastic License 2.0 and cloud pricing bills database sources per GB.

Estuary Flow captures MySQL via the binary log, backfills first, then streams. Its Snowflake materialization applies changes transactionally and can hard delete rows when the option is on. It is hosted and priced per GB plus a monthly fee per connector, with private deployment on annual contract only.

Debezium is the reference binlog reader, but it has no Snowflake sink. Debezium Server lists Kafka, Kinesis, Pub/Sub, JDBC, and Iceberg, and Snowflake is absent. The path runs through Kafka to the Snowflake Connector for Kafka, which lands rows as VARIANT and does not reflect deleted messages. You write the MERGE yourself.

dlt, Sling, and ingestr round out the list. dlt's sql_database source reads MySQL by query, with no binlog CDC documented on that page. Sling reads the binlog but CDC requires a CLI Pro Max token. ingestr, a Go codebase at version 1.x, streams the binlog under the mysql+cdc scheme. Its LICENSE is the Functional Source License 1.1, not the MIT its docs index still claims.

Hevo and Stitch are hosted, log-based, and metered by events and rows. AWS DMS does not target Snowflake at all. Its target list stops at Redshift, S3, Kinesis, and Kafka.

Where does Filament fit for MySQL to Snowflake?

Filament is an Apache 2.0 replication engine written in Go, and Galaxy builds it. Measured against the six criteria above, it answers five with documented behavior and leaves warehouse wake-ups to the schedule you set.

Deletes and consistent start. The MySQL source bootstraps CDC from a consistent snapshot. For a table with no checkpoint, the stream "continues from the binlog position captured just before that snapshot opened." Table locks last "for the instant between capturing the position and opening the snapshot." The Snowflake sink offers cdc_merge, which applies each batch "with one Snowflake MERGE," so deleted MySQL rows are deleted in Snowflake.

Where the data runs. Filament runs as one binary, a Go module inside your service, or a Helm chart. The Snowflake sink uploads each Parquet batch "with PUT to a run-scoped path under the user's @~ stage." The same page adds that "no external storage integration is required." Rows go from your MySQL to your Snowflake stage and nowhere else.

Auth. The sink supports key-pair authentication and Snowflake programmatic access tokens. Its docs say "password authentication is not exposed," which matches where Snowflake is heading.

Integrity and restarts. The integrity page says that before every write "the pipeline calculates a CRC32-C checksum over the batch's Arrow buffers," and the sink recalculates it. Snapshots save the last key, so an interrupted copy resumes, and large tables split into as many as 64 primary-key ranges read in parallel. CDC runs stream from the checkpoint to the current position and exit, so continuous replication is a cron entry.

Maturity and speed deserve two plain sentences. The MySQL source is beta and the Snowflake sink is alpha today, per the connector overview. The public benchmark cohort has no MySQL to Snowflake run, so this route carries no speed claim.

How to replicate MySQL to Snowflake with Filament

What you need. MySQL 8.0 or later with InnoDB tables, a user holding REPLICATION SLAVE, REPLICATION CLIENT, SELECT, and LOCK TABLES, and binlog retention that covers the snapshot. Install the CLI with curl -fsSL https://getgalaxy.io/filament/install | sh or brew install galaxy-io/tap/filament, per the CLI page. The local CLI uses file-backed SQLite and needs no other service.

Prepare Snowflake. The sink page ships the full setup script. It creates a role, an X-Small warehouse with one minute auto-suspend, a database, and a SERVICE user with an RSA public key attached.

USE ROLE SECURITYADMIN;
CREATE ROLE IF NOT EXISTS FILAMENT_ROLE;
GRANT ROLE FILAMENT_ROLE TO ROLE SYSADMIN;
USE ROLE SYSADMIN;
CREATE WAREHOUSE IF NOT EXISTS FILAMENT_WAREHOUSE
    WAREHOUSE_SIZE = XSMALL
    AUTO_SUSPEND = 60
    AUTO_RESUME = TRUE
    INITIALLY_SUSPENDED = TRUE;
CREATE DATABASE IF NOT EXISTS FILAMENT_DATABASE;
GRANT USAGE ON WAREHOUSE FILAMENT_WAREHOUSE TO ROLE FILAMENT_ROLE;
GRANT USAGE, CREATE SCHEMA ON DATABASE FILAMENT_DATABASE TO ROLE FILAMENT_ROLE;
GRANT USAGE, CREATE TABLE ON SCHEMA FILAMENT_DATABASE.PUBLIC TO ROLE FILAMENT_ROLE;
USE ROLE SECURITYADMIN;
CREATE USER IF NOT EXISTS FILAMENT_USER
    TYPE = SERVICE
    DEFAULT_ROLE = FILAMENT_ROLE
    DEFAULT_WAREHOUSE = FILAMENT_WAREHOUSE;
GRANT ROLE FILAMENT_ROLE TO USER FILAMENT_USER;
ALTER USER FILAMENT_USER SET RSA_PUBLIC_KEY = '<PUBLIC_KEY_BODY>'

USE ROLE SECURITYADMIN;
CREATE ROLE IF NOT EXISTS FILAMENT_ROLE;
GRANT ROLE FILAMENT_ROLE TO ROLE SYSADMIN;
USE ROLE SYSADMIN;
CREATE WAREHOUSE IF NOT EXISTS FILAMENT_WAREHOUSE
    WAREHOUSE_SIZE = XSMALL
    AUTO_SUSPEND = 60
    AUTO_RESUME = TRUE
    INITIALLY_SUSPENDED = TRUE;
CREATE DATABASE IF NOT EXISTS FILAMENT_DATABASE;
GRANT USAGE ON WAREHOUSE FILAMENT_WAREHOUSE TO ROLE FILAMENT_ROLE;
GRANT USAGE, CREATE SCHEMA ON DATABASE FILAMENT_DATABASE TO ROLE FILAMENT_ROLE;
GRANT USAGE, CREATE TABLE ON SCHEMA FILAMENT_DATABASE.PUBLIC TO ROLE FILAMENT_ROLE;
USE ROLE SECURITYADMIN;
CREATE USER IF NOT EXISTS FILAMENT_USER
    TYPE = SERVICE
    DEFAULT_ROLE = FILAMENT_ROLE
    DEFAULT_WAREHOUSE = FILAMENT_WAREHOUSE;
GRANT ROLE FILAMENT_ROLE TO USER FILAMENT_USER;
ALTER USER FILAMENT_USER SET RSA_PUBLIC_KEY = '<PUBLIC_KEY_BODY>'

USE ROLE SECURITYADMIN;
CREATE ROLE IF NOT EXISTS FILAMENT_ROLE;
GRANT ROLE FILAMENT_ROLE TO ROLE SYSADMIN;
USE ROLE SYSADMIN;
CREATE WAREHOUSE IF NOT EXISTS FILAMENT_WAREHOUSE
    WAREHOUSE_SIZE = XSMALL
    AUTO_SUSPEND = 60
    AUTO_RESUME = TRUE
    INITIALLY_SUSPENDED = TRUE;
CREATE DATABASE IF NOT EXISTS FILAMENT_DATABASE;
GRANT USAGE ON WAREHOUSE FILAMENT_WAREHOUSE TO ROLE FILAMENT_ROLE;
GRANT USAGE, CREATE SCHEMA ON DATABASE FILAMENT_DATABASE TO ROLE FILAMENT_ROLE;
GRANT USAGE, CREATE TABLE ON SCHEMA FILAMENT_DATABASE.PUBLIC TO ROLE FILAMENT_ROLE;
USE ROLE SECURITYADMIN;
CREATE USER IF NOT EXISTS FILAMENT_USER
    TYPE = SERVICE
    DEFAULT_ROLE = FILAMENT_ROLE
    DEFAULT_WAREHOUSE = FILAMENT_WAREHOUSE;
GRANT ROLE FILAMENT_ROLE TO USER FILAMENT_USER;
ALTER USER FILAMENT_USER SET RSA_PUBLIC_KEY = '<PUBLIC_KEY_BODY>'

Generate the key with openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out snowflake_filament_snow_key.p8 -nocrypt and paste the public key body into the last line.

Declare the pipeline. The CLI reads ~/.config/filament/filament.yaml. The block below uses the field names from the MySQL source and Snowflake sink pages. The docs' own YAML example is Postgres to stdout, so confirm the nested auth keys against filament sink create --sink-connector snowflake --help, which lists every connector field.

version: 1

sources:
  app_mysql:
    type: mysql
    config:
      connection_method: url
      dsn: env:MYSQL_DSN
      replication: cdc

sinks:
  warehouse:
    type: snowflake
    config:
      connection_method: fields
      account: myorg-myaccount
      username: FILAMENT_USER
      warehouse: FILAMENT_WAREHOUSE
      database_name: FILAMENT_DATABASE
      schema_name: PUBLIC
      role: FILAMENT_ROLE
      auth:
        type: key_pair
        private_key: env:SNOWFLAKE_PRIVATE_KEY

pipelines:
  app-to-snowflake:
    source:
      ref: app_mysql
    sink:
      ref: warehouse
    resources:
      - customers
      - orders
      - order_items
    sync_mode: cdc
    write_mode

version: 1

sources:
  app_mysql:
    type: mysql
    config:
      connection_method: url
      dsn: env:MYSQL_DSN
      replication: cdc

sinks:
  warehouse:
    type: snowflake
    config:
      connection_method: fields
      account: myorg-myaccount
      username: FILAMENT_USER
      warehouse: FILAMENT_WAREHOUSE
      database_name: FILAMENT_DATABASE
      schema_name: PUBLIC
      role: FILAMENT_ROLE
      auth:
        type: key_pair
        private_key: env:SNOWFLAKE_PRIVATE_KEY

pipelines:
  app-to-snowflake:
    source:
      ref: app_mysql
    sink:
      ref: warehouse
    resources:
      - customers
      - orders
      - order_items
    sync_mode: cdc
    write_mode

version: 1

sources:
  app_mysql:
    type: mysql
    config:
      connection_method: url
      dsn: env:MYSQL_DSN
      replication: cdc

sinks:
  warehouse:
    type: snowflake
    config:
      connection_method: fields
      account: myorg-myaccount
      username: FILAMENT_USER
      warehouse: FILAMENT_WAREHOUSE
      database_name: FILAMENT_DATABASE
      schema_name: PUBLIC
      role: FILAMENT_ROLE
      auth:
        type: key_pair
        private_key: env:SNOWFLAKE_PRIVATE_KEY

pipelines:
  app-to-snowflake:
    source:
      ref: app_mysql
    sink:
      ref: warehouse
    resources:
      - customers
      - orders
      - order_items
    sync_mode: cdc
    write_mode

Set MYSQL_DSN to a mysql:// URL and SNOWFLAKE_PRIVATE_KEY to the PEM contents, then run the pipeline.

What the first run produces. For each table, Filament creates a typed table in the target schema, mapping integers to NUMBER(38,0), timestamps to TIMESTAMP_NTZ(6), and JSON to VARIANT. It captures the binlog position, opens the snapshot, stages each batch as Parquet, loads it with COPY INTO, and removes the staged file. Then it applies the changes that landed during the copy. Rows carry _filament_ columns with run ID, operation, and position.

Keeping it current. Add a schedule to the pipeline, and each run catches up from the saved GTID or file position and exits. A run with no changes still advances the watermark. Delivery is at-least-once and merge converges by primary key, so a crash mid-transaction replays that transaction safely. Each COPY INTO wakes the warehouse, so a five-minute cron with the auto-suspend above keeps compute idle most of the day.

What breaks in a MySQL to Snowflake pipeline?

The rows rarely fail. The types do. These are the traps every tool on this route documents, with the fix for each.

Trap

MySQL behavior

Snowflake behavior

Fix

Zero dates

Stores 0000-00-00

No such value

Convert to NULL, not the epoch

TINYINT(1)

Synonym for BOOL

BOOLEAN or NUMBER, tool decides

Choose per column before loading

DECIMAL over 38 digits

Up to 65 digits

38 digit maximum

Widen to text or round on purpose

DATETIME

No time zone stored

TIMESTAMP_NTZ

Set the session zone once

TIMESTAMP

Stored as UTC, shown in session zone

TIMESTAMP_TZ

Map to a zoned type

ENUM and SET

Index plus label

TEXT

Expect the label, not the index

JSON

Native JSON type

VARIANT

Load as VARIANT, not a string

Zero dates are the loudest failure. MySQL converts invalid dates to the zero value when the SQL mode permits it. Airbyte converts those to NULL, Openflow maps them to the Unix epoch, and a NOT NULL destination column rejects either. Decide before the first load, because a rewrite after data lands means a re-snapshot.

Booleans and decimals need a decision, not a default. MySQL's BOOL is a synonym for TINYINT(1), and vendors disagree on the mapping. Airbyte emits boolean by default while Openflow emits INT. MySQL DECIMAL allows far more digits than Snowflake's NUMBER, and Openflow says "precision is lost when exceeded." Filament writes such columns as TEXT instead of rounding.

Lost logs and schema changes end the run. If the binlog is purged before the snapshot finishes, ingestr "fails instead of taking a partial snapshot," Fivetran requires a re-sync, and Filament re-snapshots the table. A column-count mismatch after DDL fails a Filament run rather than mis-mapping values, so an incompatible ALTER TABLE also means a re-snapshot of that table.

Which MySQL to Snowflake option fits which job?

Pick by the constraint that breaks first.

Job

Pick

Why

Replica inside your network, deletes propagated

Filament

Snapshot plus binlog from one point, merge removes deletes, checksummed batches, no meter

Everything inside Snowflake, no software to run

Openflow

Generally available, native MERGE, billed as Snowflake credits

Widest catalog, budget by rows

Fivetran

Binlog or Teleport, user stage staging, MAR meter

Hosted CDC with hard deletes

Estuary Flow

Transactional apply, hard delete option, per GB

Kafka already in production

Debezium plus Kafka connector

The change stream is already there, MERGE is yours

Code-first Python, deletes not required

dlt

Apache 2.0, many merge strategies, query-based reads

For the main reader, a team that wants a current Snowflake copy of production MySQL without sending rows through a vendor, Filament is the pick. It answers deletes, consistent start, locality, auth, and integrity with documented behavior and runs as one process. A team that lives entirely inside Snowflake should take Openflow. A team that will not operate software should take a hosted service and set the auto-suspend low.

Related guides

Frequently asked questions

How do I replicate MySQL to Snowflake?

Snapshot each table, load it into Snowflake through a stage and COPY INTO, then read the MySQL binary log and apply inserts, updates, and deletes with MERGE. Snowflake's Openflow connector, hosted services like Fivetran and Estuary, and self-hosted tools like Debezium, Sling, ingestr, and Filament all follow this pattern.

Can MySQL replicate to Snowflake in real time?

Close to it. Log-based CDC reads the binlog as changes commit, but every tool then batches those changes into Snowflake, because each load wakes a warehouse that bills per second with a one minute minimum. Most teams run a catch-up every few minutes rather than streaming row by row, which keeps latency low and compute predictable.

Does MySQL to Snowflake CDC capture deletes?

Yes, if the tool reads the binary log. A timestamp-based incremental sync cannot see a hard delete because the row is gone. Tools differ in how they apply deletes. Fivetran and Openflow flag the row as deleted, Estuary and Sling can remove it or flag it, and Filament's merge mode removes it.

What binlog retention should I set for MySQL to Snowflake?

Long enough to cover the initial snapshot plus any pause in syncing. MySQL's own default is 30 days, but Amazon RDS retains nothing by default and caps retention at one week. Fivetran asks for at least a day, Estuary and Airbyte suggest a week or more. Set it before the first run, since a purged log forces a full re-snapshot.

Does the initial snapshot lock my MySQL tables?

Briefly, on most tools. Log-based replicators need one consistent point where the snapshot and the binlog position agree, and hosted MySQL services do not allow a global read lock, so tools take short table-level locks instead. Writers wait a moment while the position is captured, then the snapshot runs without blocking anyone.

What MySQL types break when loading into Snowflake?

Zero dates, which Snowflake rejects. TINYINT(1) columns that MySQL treats as booleans. DECIMAL columns beyond 38 digits, which Snowflake cannot hold. DATETIME columns with no time zone. ENUM and SET values, which land as text. JSON, which needs a VARIANT column rather than a string. Check each of these before the first load.

Does Snowflake still allow password login for replication tools?

Not for service users. Snowflake's own rollout says it will disallow passwords for all service users, and Fivetran, Airbyte, Estuary, and Filament all document key-pair authentication as the required or recommended path. Generate a 2048 bit RSA key pair, attach the public key to a SERVICE user, and hand the private key to the tool.

How do I set up MySQL to Snowflake replication with Filament?

Create a MySQL source with replication set to cdc, a Snowflake sink with key-pair auth, and a pipeline listing the tables with the merge write mode. The first run snapshots each table, stages Parquet to your own user stage, loads it with COPY INTO, and streams the binlog from the position captured before the snapshot. Later runs catch up from the saved checkpoint.

Does Filament need an S3 bucket to load Snowflake?

No. Its Snowflake sink encodes each batch as Parquet in memory, uploads it with PUT to a run-scoped path under the Snowflake user's internal stage, loads it with COPY INTO, and removes the staged object. No external storage integration is required, so no bucket, IAM role, or storage integration sits between MySQL and Snowflake.

Which tool is best for MySQL to Snowflake?

It depends on the constraint that breaks first. A team already inside Snowflake with no software to run should look at Openflow. A team that will pay per row for a managed service can take Fivetran or Estuary. A team that wants the replica to run in its own network, propagate deletes, and verify every batch can use Filament, with the Snowflake sink at alpha today.

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.