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.



Check editions before you promise a timeline.


Openflow vs ADF vs a paid connector: the decision table


AxisOpenflow (SQL Server CDC)Azure Data FactoryPaid connector (MAR-billed)
Where compute runsSnowflake-managed SPCS runtime, or BYOC in your AWS accountAzure IR for data flows, self-hosted IR for copyVendor SaaS or agent
Cloud availabilitySnowflake deployment on AWS, Azure, GCP; BYOC on AWS onlyAzure onlyVendor's regions
On-prem source behind a firewallRuntime needs network reach to the sourceData flows cannot use the self-hosted IRAgent handles it
Initial snapshotFull, then incremental, or skippedYou build itAutomatic, history usually free
Latency floorContinuous, gated by your merge scheduleSub-minute in the CDC resource, else batchPlan-dependent, minutes
Pricing unitCredits: Openflow compute, Snowpipe Streaming, warehousevCore-hours, activity runs, DIU-hoursMonthly active rows, per connection
Ops skillNiFi, runtime sizing, Snowflake adminADF, which your team knowsAlmost none
Snowflake as a CDC-resource targetN/ANot supported todayNative

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.


T-SQL - enable CDC and grant the connector's login
-- 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.


Snowflake SQL - destination objects for the connector
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:



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.


Snowflake SQL - watch all three meters
-- 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.


Snowflake SQL - collapse an ADF-landed CDC batch and apply 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:



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:



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.


SymptomCauseFix
Snapshot completes, then zero incremental rowsdb_datareader granted, no SELECT on the cdc schemaGRANT SELECT ON SCHEMA::cdc, restart replication
Table never enters replicationNo primary key, unique constraint or index, or logical keyDeclare a logical key, or add a unique index
Table moves to FAILED and stays thereValue over the 16 MB limit, or unsupported type changeRemove the table, fix the source, re-add it
Row counts climb above sourceDeletes are soft deletesFilter on _SNOWFLAKE_DELETED = FALSE in every view
Source truncated, Snowflake unchangedTruncate is not supportedRe-seed: remove and re-add the table
Primary key changed, replication wrongKey changes are not detected at runtimeRestart replication for that table manually
ADF copy fails at the staging stepStaging not using SAS auth, no storage integrationSwitch staging to SAS auth, or set a storage integration
ADF data flow cannot see the on-prem sourceData flows run on the Azure IR onlyManaged 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



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.


Pranay Vatsal, Founder & CEO

Pranay Vatsal is the Founder & CEO of CelestInfo with deep expertise in Snowflake, data architecture, and building production-grade data systems for global enterprises.

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.