SQL Server CDC to Snowflake: Openflow vs ADF vs a Paid Connector
Quick answer: For SQL Server CDC to Snowflake you now have three credible paths. Snowflake's Openflow Connector for SQL Server (CDC) went generally available on 5 August 2026 and replicates from SQL Server's native CDC change tables, billed in Snowflake credits. Azure Data Factory can do it, but its native CDC runs on the Azure integration runtime only, so an on-prem source needs private networking or a copy-then-merge pattern; paid connectors stay the lowest-effort option and bill on monthly active rows.
Last updated: August 2026
Six days ago, on 5 August 2026, Snowflake marked the Openflow Connector for SQL Server (CDC) generally available. That matters more than a typical connector release: it is the first time "replace the CDC tool" is a conversation an enterprise Azure shop can have with a straight face. Until now the honest answer to "how do we get SQL Server into Snowflake continuously" was either pay Fivetran, or build it in ADF and accept a batch pipeline wearing a CDC costume.
This is not a Snowflake pitch. Openflow is Apache NiFi underneath, it runs on infrastructure you size and monitor, and its bring-your-own-cloud deployment is still AWS-only. ADF has a limitation almost nobody writes about: its native CDC lives in mapping data flows, and mapping data flows do not run on the self-hosted integration runtime. If your SQL Server sits behind a firewall, that one line reshapes the whole design.
What changed on 5 August 2026
The Openflow Connector for SQL Server (CDC) reached general availability. It reads the change tables SQL Server's Change Data Capture feature populates and applies row-level inserts, updates and deletes into Snowflake, continuously or on a schedule. Because it reads change tables rather than polling current state, it captures every row-level change including intermediate states between polls.
Availability splits by deployment model, not by connector. Openflow Snowflake Deployments, on Snowpark Container Services, are available in AWS, Azure and GCP commercial regions. Openflow BYOC, where the data plane runs in your own cloud account, is AWS commercial regions only, so Azure shops wanting the data plane in their own VNet are out of luck.
Openflow ships two SQL Server connectors and they are not interchangeable
There is an Openflow Connector for SQL Server and an Openflow Connector for SQL Server (CDC), and they have different source requirements.
- Connector for SQL Server. Uses Change Tracking, which reports the net effect of changes between polls. Update a row four times and you get one row with the final state. Fine for a replica, useless if you need intermediate versions.
- Connector for SQL Server (CDC). Uses Change Data Capture, reading the
cdcschema change tables the capture job fills from the transaction log. This is the one that went GA on 5 August 2026. - Edition trap. The documented source list covers Enterprise, Standard and Developer editions only, with SQL Server 2016 Standard needing SP1 or later. Versions run 2016 (SP1+) through 2025, plus Azure SQL Database, Azure SQL Managed Instance, AWS RDS for SQL Server and Google Cloud SQL for SQL Server.
Check editions before you promise a timeline.
Openflow vs ADF vs a paid connector: the decision table
| Axis | Openflow (SQL Server CDC) | Azure Data Factory | Paid connector (MAR-billed) |
|---|---|---|---|
| Where compute runs | Snowflake-managed SPCS runtime, or BYOC in your AWS account | Azure IR for data flows, self-hosted IR for copy | Vendor SaaS or agent |
| Cloud availability | Snowflake deployment on AWS, Azure, GCP; BYOC on AWS only | Azure only | Vendor's regions |
| On-prem source behind a firewall | Runtime needs network reach to the source | Data flows cannot use the self-hosted IR | Agent handles it |
| Initial snapshot | Full, then incremental, or skipped | You build it | Automatic, history usually free |
| Latency floor | Continuous, gated by your merge schedule | Sub-minute in the CDC resource, else batch | Plan-dependent, minutes |
| Pricing unit | Credits: Openflow compute, Snowpipe Streaming, warehouse | vCore-hours, activity runs, DIU-hours | Monthly active rows, per connection |
| Ops skill | NiFi, runtime sizing, Snowflake admin | ADF, which your team knows | Almost none |
| Snowflake as a CDC-resource target | N/A | Not supported today | Native |
The last row is the one people get wrong. ADF's Change Data Capture resource lists Snowflake as a supported source, but its documented targets are Avro, Azure SQL Database, SQL Managed Instance, Delimited Text, Delta, JSON, ORC, Parquet and Azure Synapse Analytics. Snowflake is not among them, and the resource is in preview.
What the Openflow CDC connector needs before it will run
The source-side work is ordinary DBA work, and it stalls projects because it needs elevated rights on production. Enabling CDC at database level creates the capture and cleanup agent jobs; enabling it per table creates a capture instance and change table.
-- 1. Enable CDC on the database. Needs sysadmin (or db_owner) on the instance.
USE SalesDB;
GO
EXEC sys.sp_cdc_enable_db;
GO
-- 2. Enable CDC per table. Repeat for every table you plan to replicate.
-- This creates a capture instance and the cdc.dbo_Orders_CT change table.
EXEC sys.sp_cdc_enable_table
@source_schema = N'dbo',
@source_name = N'Orders',
@role_name = NULL;
GO
-- 3. Create the login the connector authenticates with.
USE master;
GO
CREATE LOGIN openflow_reader WITH PASSWORD = 'put-a-real-secret-here';
GO
-- 4. Create the user in EVERY source database, then grant read on both the
-- base tables and the cdc schema. Skipping the second grant is the single
-- most common setup failure: the connector connects, snapshots fine, and
-- then silently reads nothing on the incremental phase.
USE SalesDB;
GO
CREATE USER openflow_reader FOR LOGIN openflow_reader;
ALTER ROLE db_datareader ADD MEMBER openflow_reader;
GRANT SELECT ON SCHEMA::cdc TO openflow_reader;
GO
-- 5. Verify. is_cdc_enabled must be 1 and both CDC agent jobs must exist.
SELECT name, is_cdc_enabled FROM sys.databases WHERE name = 'SalesDB';
EXEC sys.sp_cdc_help_jobs;
GO
The grant on the cdc schema is the one people forget. db_datareader covers the base tables, enough for the snapshot to succeed and everything to look healthy, but not the change tables, so incremental replication then quietly does nothing.
The Snowflake side needs a service user with key-pair authentication, a role, a destination database the connector can create schemas in, and a warehouse.
USE ROLE ACCOUNTADMIN;
-- Destination database. The connector creates schemas under it, so it needs
-- CREATE SCHEMA, not just USAGE.
CREATE DATABASE IF NOT EXISTS SQLSERVER_REPL;
CREATE ROLE IF NOT EXISTS OPENFLOW_SQLSERVER_ROLE;
-- Service-type user with key-pair auth. Openflow does not use a password here.
CREATE USER IF NOT EXISTS OPENFLOW_SQLSERVER_USER
TYPE = SERVICE
DEFAULT_ROLE = OPENFLOW_SQLSERVER_ROLE
RSA_PUBLIC_KEY = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...'
COMMENT = 'Service user for the Openflow SQL Server CDC connector';
GRANT ROLE OPENFLOW_SQLSERVER_ROLE TO USER OPENFLOW_SQLSERVER_USER;
GRANT USAGE ON DATABASE SQLSERVER_REPL TO ROLE OPENFLOW_SQLSERVER_ROLE;
GRANT CREATE SCHEMA ON DATABASE SQLSERVER_REPL TO ROLE OPENFLOW_SQLSERVER_ROLE;
-- This warehouse runs the MERGE from journal table to destination table.
-- It is a separate meter from Openflow compute. Give each connector its own
-- warehouse or you will never attribute the spend.
CREATE WAREHOUSE IF NOT EXISTS OPENFLOW_WH
WAREHOUSE_SIZE = 'XSMALL'
AUTO_SUSPEND = 60
AUTO_RESUME = TRUE
INITIALLY_SUSPENDED = TRUE;
GRANT USAGE, OPERATE ON WAREHOUSE OPENFLOW_WH TO ROLE OPENFLOW_SQLSERVER_ROLE;
One prerequisite catches people at deployment: the runtime must be at least Medium, and the connector does not support multi-node runtimes, so Min nodes and Max nodes both stay at 1. You can, though, run several CDC connector instances on a single runtime by sharing the Source and Destination parameter contexts and overriding only the ingestion values per connector. That is good for the credit bill and awkward for chargeback, which is the next section's problem.
How Openflow lands the data, and the three meters it bills
The write path is what lets you predict cost. Per source table the connector keeps two objects: a destination table holding current state, and an append-only journal table holding the full change history, named <TABLE_NAME>_JOURNAL_<timestamp>_<number>, where the timestamp is Unix epoch and the number increments each time the table's schema changes. Events stream into the journal via Snowpipe Streaming; a periodic MERGE applies the journal to the destination.
That MERGE is driven by the Merge Task Schedule CRON parameter. Use * * * * * ? for continuous merges, or a window so the warehouse is not running all day. Snowflake evaluates it in UTC, a detail that will bite exactly one person on your team at exactly the wrong time. Three meters run concurrently:
- Openflow compute. It meters as service type
OPENFLOW_COMPUTE_SNOWFLAKEorOPENFLOW_COMPUTE_BYOCdepending on the deployment model. BYOC bills per second with a 60 second minimum and charges credits for runtime usage only, while your own cloud provider bills the nodes underneath. On a Snowflake deployment both the data plane and the runtime land on the credit bill. - Snowpipe Streaming. Every change event goes through the streaming path, its own serverless meter, plus temporary file storage while data is in flight.
- Your warehouse. The journal-to-destination MERGE runs on the warehouse you granted the connector. Continuous CRON across a hundred tables means it never suspends.
Attribution is better than people expect, but it is not free. SNOWFLAKE.ACCOUNT_USAGE.OPENFLOW_USAGE_HISTORY reports hourly credits per runtime, separating RUNTIME_CREDITS_USED from DATA_PLANE_CREDITS_USED and tagging each row with a DATA_PLANE_TYPE of SNOWFLAKE or BYOC. What it will not do is split one runtime across the connectors sharing it. If finance wants per-source chargeback, give each source its own runtime and its own warehouse, and accept that you are buying clarity with credits.
-- Meter 1a: account-level Openflow compute, both deployment models.
SELECT
usage_date,
service_type,
SUM(credits_used) AS credits
FROM snowflake.account_usage.metering_daily_history
WHERE service_type IN ('OPENFLOW_COMPUTE_SNOWFLAKE', 'OPENFLOW_COMPUTE_BYOC')
AND usage_date >= DATEADD('day', -30, CURRENT_DATE())
GROUP BY usage_date, service_type
ORDER BY usage_date DESC;
-- Meter 1b: the same spend broken out per runtime. This is the view that makes
-- chargeback possible. It still cannot split one runtime across the connectors
-- sharing it, so one runtime per source is the price of a clean answer.
SELECT
DATE_TRUNC('day', start_time) AS usage_day,
runtime_name,
data_plane_type,
SUM(runtime_credits_used) AS runtime_credits,
SUM(data_plane_credits_used) AS data_plane_credits
FROM snowflake.account_usage.openflow_usage_history
WHERE start_time >= DATEADD('day', -30, CURRENT_TIMESTAMP())
GROUP BY 1, 2, 3
ORDER BY usage_day DESC, runtime_credits DESC;
-- Meter 2: Snowpipe Streaming, which is how the connector writes the rows.
SELECT
usage_date,
SUM(credits_used) AS credits
FROM snowflake.account_usage.metering_daily_history
WHERE service_type = 'SNOWPIPE_STREAMING'
AND usage_date >= DATEADD('day', -30, CURRENT_DATE())
GROUP BY usage_date
ORDER BY usage_date DESC;
-- Meter 3: your warehouse, running the journal-to-destination MERGE.
SELECT
DATE_TRUNC('day', start_time) AS usage_day,
warehouse_name,
SUM(credits_used) AS credits
FROM snowflake.account_usage.warehouse_metering_history
WHERE warehouse_name = 'OPENFLOW_WH'
AND start_time >= DATEADD('day', -30, CURRENT_TIMESTAMP())
GROUP BY 1, 2
ORDER BY usage_day DESC;
Where ADF actually stands for SQL Server CDC into Snowflake
ADF offers three genuinely different things that get conflated: native change data capture in mapping data flows, itself still marked preview, where a SQL Server source picks up inserts, updates and deletes with no watermark column once CDC is enabled at source; the Change Data Capture resource, also in public preview, which runs continuously and bills four v-cores of General Purpose data flows; and the copy-plus-watermark pattern most production estates actually run.
The integration runtime decides your design. Microsoft's IR capability table is explicit: the Azure IR supports Data Flow, data movement and activity dispatch; the self-hosted IR supports data movement and activity dispatch only. So for an on-prem SQL Server behind a firewall, ADF's native CDC is not directly reachable. Put managed VNet and a private endpoint in front of the source, or run a copy activity on the self-hosted IR and process the landed rows separately.
The sink has its own requirements. ADF's Snowflake connector drives COPY INTO <table>, so unless the data already sits in blob storage in a compatible format, ADF stages through interim Azure Blob Storage. The Snowflake account needs USAGE on the database, read and write on the schema, and CREATE STAGE to build the external stage with a SAS URI. Without a storage integration, staging must use SAS authentication, because Snowflake's COPY command requires it.
-- The ADF Copy activity reads cdc.fn_cdc_get_all_changes_dbo_Orders(...) over
-- the self-hosted IR and lands raw change rows here. Rename the __$ columns in
-- the copy activity's column mapping so downstream SQL stays readable.
CREATE TABLE IF NOT EXISTS raw.orders_cdc_landing (
cdc_start_lsn BINARY,
cdc_seqval BINARY,
cdc_operation NUMBER(1,0), -- 1 delete, 2 insert, 3 pre-update, 4 post-update
order_id NUMBER(38,0),
customer_id NUMBER(38,0),
order_status VARCHAR(32),
total_amount NUMBER(18,2),
modified_at TIMESTAMP_NTZ,
loaded_at TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP()
);
-- Collapse the batch to one row per key in LSN order, then apply it once.
-- Without the ROW_NUMBER, a batch holding insert-then-update-then-delete for
-- the same key makes the MERGE non-deterministic and Snowflake errors out.
MERGE INTO core.orders AS t
USING (
SELECT *
FROM (
SELECT
s.*,
ROW_NUMBER() OVER (
PARTITION BY s.order_id
ORDER BY s.cdc_start_lsn DESC, s.cdc_seqval DESC
) AS rn
FROM raw.orders_cdc_landing s
WHERE s.cdc_operation IN (1, 2, 4) -- discard the pre-update image
)
WHERE rn = 1
) AS c
ON t.order_id = c.order_id
WHEN MATCHED AND c.cdc_operation = 1 THEN DELETE
WHEN MATCHED THEN UPDATE SET
t.customer_id = c.customer_id,
t.order_status = c.order_status,
t.total_amount = c.total_amount,
t.modified_at = c.modified_at
WHEN NOT MATCHED AND c.cdc_operation <> 1 THEN INSERT
(order_id, customer_id, order_status, total_amount, modified_at)
VALUES
(c.order_id, c.customer_id, c.order_status, c.total_amount, c.modified_at);
The ROW_NUMBER is not optional. A CDC batch routinely holds several operations for the same key, and a MERGE whose USING clause returns duplicate join keys is non-deterministic, so Snowflake errors rather than picking one. Most hand-built pipelines that fail intermittently in production fail here first.
What you are actually buying with a paid connector
Managed connectors have not become irrelevant; what changed is that you can now price them against something. Fivetran's unit is the monthly active row: distinct rows synced to the destination in a calendar month, tracked by distinct primary keys, with a synthetic hashed key when a table has none. A row counts once per month however often it syncs. Four details change the arithmetic:
- Initial syncs do not incur cost for the historical data they load. The backfill is not what you pay for. Incremental updates after it are.
- Deletes count. Inserts, updates and deletes are all charged, so a nightly purge job on a large table is a recurring line item.
- MAR is per connection. Two connections pulling the same table with the same primary keys count separately, so multi-environment setups double.
- There is a base charge per connection. Five dollars applies to connections between 1 and 1 million MAR a month. Fifty lookup tables as fifty connections is a floor you pay before any volume.
I am deliberately not quoting a per-thousand-MAR rate, because those move and the only rate that matters is the one on your contract. Model it on rows that actually change per month, not on table size. Teams over-estimate badly, thinking in row counts when the meter thinks in churn.
Schema drift is what decides this in year two
Everyone evaluates on latency and price. What determines whether the pipeline is alive in eighteen months is what happens when a developer adds a column. The connector handles adding, dropping and re-adding columns, compatible type changes such as INT to BIGINT (both land as NUMBER), numeric precision and scale changes, and character length changes. It absorbs them by moving to a new SQL Server capture instance and draining the old one, which is worth knowing because SQL Server allows at most two capture instances on a source table at a time. Three changes it does not handle: a primary key definition change, an incompatible type change such as INT to VARCHAR, and a column rename. The rename is the one teams assume is safe.
Three behaviours are worth knowing before someone reports them as bugs:
- Nothing is ever deleted. A row deleted at source is soft-deleted: the connector flags
_SNOWFLAKE_DELETEDrather than removing it. Destination tables also carry_SNOWFLAKE_INSERTED_ATand_SNOWFLAKE_UPDATED_AT. Every downstream view needs aWHERE _SNOWFLAKE_DELETED = FALSEpredicate or counts drift upward forever. - Dropped columns are renamed, not removed. A dropped source column gets a
__SNOWFLAKE_DELETEDsuffix. Journal tables are never cleaned automatically either, since Snowflake keeps them for auditing and reprocessing. - New columns do not backfill. Rows that existed before the change keep NULL in the new column. The deeper cause sits upstream: SQL Server's capture process ignores any column that was not identified for capture when the table was enabled, so a new column carries no history until a capture instance exists that includes it.
Failure modes and what the symptom actually means
This is the section you will come back to. The connector tracks each table in a state store with the states NEW, SNAPSHOT_REPLICATION, INCREMENTAL_REPLICATION and FAILED, and logs every transition, so start there.
| Symptom | Cause | Fix |
|---|---|---|
| Snapshot completes, then zero incremental rows | db_datareader granted, no SELECT on the cdc schema | GRANT SELECT ON SCHEMA::cdc, restart replication |
| Table never enters replication | No primary key, unique constraint or index, or logical key | Declare a logical key, or add a unique index |
| Table moves to FAILED and stays there | Value over the 16 MB limit, or unsupported type change | Remove the table, fix the source, re-add it |
| Row counts climb above source | Deletes are soft deletes | Filter on _SNOWFLAKE_DELETED = FALSE in every view |
| Source truncated, Snowflake unchanged | Truncate is not supported | Re-seed: remove and re-add the table |
| Primary key changed, replication wrong | Key changes are not detected at runtime | Restart replication for that table manually |
| ADF copy fails at the staging step | Staging not using SAS auth, no storage integration | Switch staging to SAS auth, or set a storage integration |
| ADF data flow cannot see the on-prem source | Data flows run on the Azure IR only | Managed VNet with a private endpoint, or copy-on-SHIR |
Every one of those recoveries is bounded by something on the SQL Server side that nobody checks until it bites: CDC retention. The cleanup job runs daily at 2 A.M. and keeps change table entries for 4320 minutes, three days, by default. Anything older is gone and a re-seed becomes your only option. Raise it with sys.sp_cdc_change_job before you need it, not during the incident. Worth knowing too: while a database has CDC enabled, the transaction log will not truncate past changes the capture process has not yet collected, so a stalled capture job eventually shows up as a disk alert.
How to choose
- Openflow CDC connector. You are a Snowflake account of meaningful size, you want ingestion spend on the Snowflake contract rather than a separate invoice, and you can build NiFi skill. The GA freshness cuts both ways: a well-documented connector with almost no community knowledge of the edge cases.
- Azure Data Factory. Your team runs ADF already and your latency requirement is tens of minutes, not seconds. Unless the source is reachable by the Azure IR, be honest that you are building a batch delta pipeline with a MERGE, not CDC.
- Paid connector. Churn is low, table count is modest, or engineering time is worth more than the MAR bill. Still the right answer for plenty of mid-market estates.
- Do not mix paths within one source estate. Two mechanisms into one database means two sets of metadata columns, two soft-delete conventions and two runbooks.
Running a proof of concept this quarter? Take your three highest-churn tables, run the connector on a Medium runtime for two weeks with a dedicated warehouse, and compare the three meters against the MAR those tables would generate. That beats a spreadsheet model.
Related Articles
Frequently Asked Questions
Q: Is the Snowflake Openflow SQL Server CDC connector generally available?
Yes. Snowflake's release notes record the Openflow Connector for SQL Server (CDC) reaching general availability on 5 August 2026. It is distinct from the earlier Openflow Connector for SQL Server, which uses Change Tracking and reports only the net effect of changes between polling intervals.
Q: Do I need SQL Server Enterprise edition to use CDC?
No. The connector's documented source list covers Enterprise, Standard and Developer editions, with SQL Server 2016 Standard requiring SP1 or later. SQL Server 2017 and later Standard editions are supported, as are Azure SQL Database, Azure SQL Managed Instance, AWS RDS for SQL Server and Google Cloud SQL for SQL Server.
Q: Can Azure Data Factory do real CDC from an on-premises SQL Server to Snowflake?
Not directly. Mapping data flows, where ADF's native CDC lives, run on the Azure integration runtime only; the self-hosted integration runtime supports data movement and activity dispatch but not Data Flow. You need managed VNet with a private endpoint, or a copy activity on the self-hosted IR feeding a separate step.
Q: Does Openflow run on Azure or only AWS?
Both, depending on deployment model. Openflow Snowflake Deployments, on Snowpark Container Services, are available in AWS, Azure and GCP commercial regions. Openflow BYOC, where the data plane runs in your own cloud account, is available in AWS commercial regions only, so Azure customers cannot yet run the data plane in their own VNet.
Q: How is Openflow billed compared with a monthly-active-rows connector?
Openflow bills in Snowflake credits across three meters: Openflow compute, Snowpipe Streaming for the ingest, and your own warehouse running the journal-to-destination MERGE. MAR connectors bill per distinct row changed per connection per month, with historical loads generally free and deletes counted.
Q: Does Openflow delete rows in Snowflake when they are deleted in SQL Server?
No. Deletes are soft deletes: the connector sets the _SNOWFLAKE_DELETED flag rather than removing the row. Dropped source columns are renamed rather than removed, and journal tables are retained for auditing and reprocessing. Filter on the delete flag in every downstream view.
