Snapshot Load
One-time full table copy when a table is first added to replication.
📷 Initial sync phase
Before CDC can track changes, Openflow performs a complete snapshot of each table. This ensures the destination starts with an exact copy of the source data.
What happens
Discover tables
Scans source database for configured tables.
Create schema
Creates matching schema in Snowflake.
Create table
Creates destination table with CDC columns.
Fetch all rows
Reads entire table contents from SQL Server.
Upload via Snowpipe
Streams data using the Snowpipe Streaming API.
Mark complete
Signals that incremental load can begin.
Key processors
| Processor | Function |
|---|---|
| MultiDatabaseFetchSourceTableSchema | Gets table DDL from SQL Server |
| UpdateSnowflakeSchema / UpdateSnowflakeTable | Creates the destination schema and table in Snowflake |
| MultiDatabaseFetchTableSnapshot | Bulk reads all rows from the source table |
| PutSnowpipeStreaming | High-throughput data loading into Snowflake |
Result: After snapshot completes, the Snowflake table contains an exact copy of all source data, and the pipeline transitions to incremental CDC mode.
Snapshot Performance & Scaling
The snapshot copies every existing row of each table into Snowflake before incremental replication starts. It is usually the first place a pipeline slows down, and a stalled snapshot blocks the table from ever reaching the incremental phase. This section covers why large tables are the bottleneck, the levers you can tune, and the signals that tell you to scale.
Why large tables are the first bottleneck
During snapshot load the connector reads each source table in primary key order,
in keyset-paged chunks, through the Fetch Table Rows [MultiDatabaseFetchTableSnapshot]
processor. For a large table this is the slowest phase, and a stalled snapshot blocks the table
from ever reaching incremental replication. Throughput comes down to how many tables you read in
parallel, the batch sizes, and the runtime's memory and disk headroom.
Levers you can tune
| Lever | Where it lives | Effect | Caution |
|---|---|---|---|
| Concurrent Select Queries For Snapshot | SQLServer Ingestion Parameters (drives Concurrent Select Queries on Fetch Table Rows) |
More concurrent source queries, speeds snapshot of many tables | Higher load on the source SQL Server database. Keep it low on a Small runtime. |
| Runtime size | Chosen when the runtime is created (Small / Medium / Large) | A larger runtime gives more disk headroom for upload backpressure and more memory | Size is fixed at creation. Changing it means migrating the connector to a new runtime. |
| Oversized Value Strategy | SQLServer Ingestion Parameters | Controls how a value larger than the connector limit is handled (pass through, set null, or fail the table) | Large values raise runtime and warehouse memory load. Exclude multi-GB LOB columns from replication instead. |
| Fetch Size / Max Batch Size | Fetch Table Rows processor properties |
Rows fetched per round trip and per FlowFile during snapshot | Larger batches use more runtime memory. Reduce for very wide rows to avoid out-of-memory. |
Runtime disk and backpressure
During snapshot, the queue in front of Upload Rows via Snowpipe Streaming can fill
with FlowFiles and trigger back pressure, which consumes runtime disk. For larger tables, use a
Large runtime for the extra storage headroom. Because runtime size is fixed at
creation, pick the size up front based on the largest tables you will snapshot.
How data reaches Snowflake
The connector loads snapshot rows into Snowflake with Snowpipe Streaming,
through the Upload Rows via Snowpipe Streaming processor, grouped per destination
table by a concurrency group. Snowpipe Streaming is serverless, so the snapshot upload itself
does not run on your warehouse. Account-level Snowpipe Streaming limits still apply as the
number of replicated tables grows.
Avoid source deadlocks: SNAPSHOT isolation
Snapshot reads run under SQL Server's default READ COMMITTED isolation and take
shared locks. If other clients hold conflicting locks on the same tables, this can deadlock and
SQL Server terminates one session. The mitigation is SNAPSHOT isolation, which
reads from row versions instead of taking shared locks:
ALTER DATABASE <database> SET ALLOW_SNAPSHOT_ISOLATION ON;
Then enable SNAPSHOT isolation on the connector's source-read processors and restart them.
Because the connector reads the source during both snapshot and incremental, this applies to
both MultiDatabaseFetchTableSnapshot and
MultiDatabaseCaptureChangeSqlServer.
Do not use RCSI. ALLOW_SNAPSHOT_ISOLATION only affects sessions
that request it (the connector). READ_COMMITTED_SNAPSHOT (RCSI) redefines the
default isolation level for every connection to the database and can change behavior for other
applications.
When to scale
- A few large tables dominate snapshot wall-clock time. Raise
Concurrent Select Queries For Snapshoton a Medium or Large runtime, and tune the fetch batch sizes. - The upload queue grows and does not drain. A persistently growing queue in front of
Upload Rows via Snowpipe Streamingsignals backpressure and heavy runtime disk use. Move to a Large runtime for storage headroom. - Instability after raising concurrency on a Small runtime. Higher concurrency assumes Medium or Large. Reduce concurrency on Small or move to a larger runtime.
- A table never transitions from SNAPSHOT_REPLICATION to INCREMENTAL_REPLICATION. Investigate a missing primary key, oversized values failing the table, or source deadlocks (enable SNAPSHOT isolation).
Runtime sizing bands and warehouse/merge tuning that span both phases are in the Incremental Performance tab.
Incremental Load
Ongoing capture of changes (INSERT, UPDATE, DELETE) after the initial snapshot.
🔄 Continuous CDC phase
After the snapshot completes, Openflow continuously polls SQL Server Change Tracking. Each polling interval captures the net effect of INSERT, UPDATE, and DELETE activity per row and replicates it to Snowflake in near real-time.
What happens
Wait for snapshot
Ensures initial load completed before starting CDC.
Read Change Tracking
Polls SQL Server CT tables for new changes.
Process schema changes
Handles DDL changes (ALTER TABLE, etc.).
Merge rows
Batches multiple changes for efficiency.
Upload to journal
Writes changes to the Snowflake journal table.
Merge to destination
Applies UPSERT/DELETE to the final table.
Data flow
Key processors
| Processor | Function |
|---|---|
| MultiDatabaseCaptureChangeSqlServer | Reads SQL Server Change Tracking tables |
| MultiDatabaseEnrichCdcStream | Processes schema changes and DDL events |
| Schedule Warehouse (MergeContent) | Bins changes and gates the merge schedule |
| PutSnowpipeStreaming | Streams changes into the journal table |
| MultiDatabaseMergeSnowflakeJournalTable | Applies UPSERT/DELETE from journal to destination |
Result: Changes appear in Snowflake within seconds of occurring in SQL Server. Change Tracking reports the net effect of changes between polls, and the journal-plus-MERGE pattern applies those changes idempotently to the destination (keyed on the primary key).
Incremental Performance & Scaling
In steady state, changes are captured through Change Tracking, streamed into per-table journal tables, then applied to destination tables by a MERGE. Loading the journals is cheap and serverless; the MERGE runs on your warehouse and is where nearly all steady-state cost and latency live. This section covers the MERGE, the retention boundary, journal growth, and how to size the runtime and warehouse.
Where the cost is: journal, then MERGE
Because the load side is serverless and the Merge Journal to Destination step runs
on a warehouse, the MERGE is the primary compute cost and the first place to look for both cost
and latency problems. Change Tracking rolls multiple updates to the same row between polls into a
single net change, which tends to make merges smaller than the CDC variant, which preserves
every intermediate operation.
Control merge cost: Merge Task Schedule CRON
The Merge Task Schedule CRON ingestion parameter governs when the merge is allowed
to run by throttling the FlowFiles that reach it. The connector evaluates it in UTC. The throttle
is demand-driven: when no changes are pending, no merge runs and the warehouse can
auto-suspend.
| Schedule | CRON | Tradeoff |
|---|---|---|
| Continuous | * * * * * ? | Lowest latency, highest steady-state warehouse time |
| Every 15 minutes | 0 */15 * * * ? | Balanced latency and cost |
| Hourly | 0 0 * * * ? | Lowest warehouse cost, higher latency and larger per-merge batches |
Watch the interaction. Throttling too aggressively lets journals accumulate
between windows, producing larger single merges that can turn a concurrency problem into a
memory problem. When reinstalling or draining queues, set the CRON back to
* * * * * ? first, or queued changes will not drain until the next window.
Warehouse sizing for merges
Snowflake recommends starting at XSMALL and adjusting from there. There are two distinct axes:
| Workload shape | Recommendation | Why |
|---|---|---|
| Getting started | XSMALL, single cluster, AUTO_SUSPEND on | Docs recommend starting XSMALL; demand-driven merges let it auto-suspend when idle. |
| Many tables (concurrency-bound) | Scale OUT: multi-cluster warehouse | Many concurrent merges are a concurrency problem; multi-cluster absorbs it better than one bigger warehouse. |
| High-frequency merges of many large rows | Scale UP: Large warehouse | Many large rows can collapse into one big MERGE that runs a smaller warehouse out of memory. |
Rule of thumb: scale out for table count, scale up for memory-bound single merges.
Change Tracking retention: a hard failure boundary
The most important operational constraint in steady state is the source retention window, set
with CHANGE_RETENTION. SQL Server purges tracked changes once they are older than
the window. If replication lag ever exceeds retention, the changes the connector needs expire,
the affected table permanently fails, and it requires a full reload (a fresh
snapshot) to resync.
ALTER DATABASE <database>
SET CHANGE_TRACKING = ON
(CHANGE_RETENTION = 5 DAYS, AUTO_CLEANUP = ON);
Set retention comfortably above your worst-case lag, not equal to it, so a paused
connector, a network outage, or a failed cycle does not force a reload. Enable Change Tracking
with TRACK_COLUMNS_UPDATED = OFF (the default): the connector does not use
column-level change info, and turning it on adds source storage and per-DML overhead for no
benefit.
Journal tables grow forever
Journal tables are retained indefinitely and never automatically cleaned up (customer data is
never auto-deleted). They live in the same schema as the destination and are named
<TABLE>_JOURNAL_<epoch>_<generation>, with a new generation on
each schema change. To reclaim storage you can truncate journals, drop journals for tables
removed from replication, or drop all but the latest generation for active tables.
Do not drop or alter active journal tables. The connector reads the latest-generation journal to drive the merge; removing or altering an active journal can cause data loss or replication failure. Only clean up journals for tables fully removed from replication.
Scaling the runtime
Runtimes are single-node and fixed-size (Small, Medium, Large), chosen at creation; the connector scales vertically, and changing size means migrating to a new runtime. Size to sustained (not peak) load. These documented bands are starting points, not service guarantees:
| Workload | Events/sec (sustained) | Active tables | Runtime |
|---|---|---|---|
| Light | below ~1,000 | fewer than ~100 | Small |
| Moderate | ~1,000 to 5,000 | hundreds | Medium |
| Heavy | ~5,000 to 15,000 | hundreds to low thousands | Large (or two Mediums for smaller blast radius) |
| Isolate | above ~15,000 from one source | any | Dedicated runtime |
Also use a dedicated runtime when you need sub-1-minute end-to-end latency or cannot tolerate noisy neighbors. Changes load into Snowflake through Snowpipe Streaming (serverless), grouped per destination table; account-level Snowpipe Streaming limits apply as the number of replicated tables grows, so factor table count into capacity planning and raise limits through Support before you approach them.
When to scale
- Merge queue depth persistently growing. FlowFiles piling up in the
Merge Journal to Destinationqueue mean merges are not keeping up. Add clusters (concurrency) or increase merge frequency; scale up size if a single table's merge is the bottleneck. - Journal storage growing in steady state. The merge side is lagging ingest. Increase merge cadence or add warehouse capacity.
- End-to-end lag rising while the source rate is flat. The connector or merge side is under-provisioned. Scale the warehouse and/or runtime.
- Spill or queueing on MERGE queries. Scale up size for spill (memory); scale out clusters for queueing (concurrency).
- Lag approaching the CHANGE_RETENTION window. You are close to permanent table failure and forced reloads. Reduce lag immediately and increase retention for headroom.
- Snowpipe Streaming account limits reached as table count grows. Plan capacity by table count and raise account limits through Support before adding more tables.
Stream Staleness Prevention
Prevents Snowflake streams from becoming stale during periods of inactivity.
⚠ Why this matters
Snowflake streams have a staleness window based on the table's data retention period. If a stream is not consumed within this window, it becomes STALE and unusable. You lose the ability to track changes and must recreate the stream.
How it works
Fetch journal streams
Query Snowflake for all journal stream metadata.
Get stream coordinates
Extract stream name and coordinates from results.
Check if streams present
Route based on whether any streams exist.
Advance STALE_AFTER
Execute SQL to refresh the staleness timestamp.
Stream lifecycle
This processor prevents staleness by periodically advancing the stream offset before the retention window elapses.
Processors
| Processor | Type | Purpose |
|---|---|---|
| Fetch Journal Streams | ExecuteSQLRecord | Queries Snowflake for all journal stream metadata |
| Get Stream Coordinates | EvaluateJsonPath | Extracts stream name and coordinates from query results |
| Check If Journal Streams Are Present | RouteOnAttribute | Routes flow based on whether any streams were found |
| Advance Stream's STALE_AFTER | ExecuteSQL | Executes SQL to advance the stream's staleness timestamp |
Scheduling: This processor group runs on a schedule (typically every few hours) to ensure streams stay fresh even during periods of inactivity on source tables.