Standard Operating Procedure

SQL Server → Azure Cosmos DB Migration

End-to-end runbook for migrating relational SQL Server workloads to Azure Cosmos DB for NoSQL. Covers schema transformation, partition key design, data movement, CDC live sync, validation, and production cut-over.

Version: 2.4.0 Audience: Senior DBA / Migration Engineer Platform: SQL Server 2016–2022 → Azure Cosmos DB for NoSQL Last Revised: 2026-07-05

Architecture Overview

Azure Cosmos DB for NoSQL stores data as JSON documents in containers, replacing SQL Server tables. Understanding the mapping between relational and document concepts is critical before any migration work begins.

Relational → Document Concept Mapping

SQL Server (Relational) Cosmos DB for NoSQL (Document) Notes
DatabaseDatabaseDirect equivalent; created via Portal or SDK
TableContainerEach container holds JSON documents
RowDocument (JSON item)Max 2 MB per document
ColumnDocument propertySchema-less; no enforced structure
Primary Keyid + Partition Keyid must be unique within a logical partition
Foreign KeyEmbedded document / reference by idNo enforced referential integrity
IndexIndexing Policy (JSON)All properties indexed by default
Stored ProcedureServer-side JavaScript (limited)Scoped to single partition
ViewQuery / materialized via Change FeedNo native view concept
SchemaNot applicableDocuments in same container can differ in shape
🔑
Partition Key Criticality: The partition key decision is permanent and cannot be changed after container creation without a full data reload. A poor partition key choice leads to hot partitions, throttling (429s), and runaway RU costs. Assess cardinality, query patterns, and write distribution before creating any container.
📦
Container Auto-Creation: The Migration Tool can auto-create containers using the assessed partition key. However, always review auto-generated container settings — throughput mode (serverless vs. provisioned), indexing policy, and TTL must be verified before migration begins.

Prerequisites — Source: SQL Server

Required Permissions

Permission / RoleScopePurposeMinimum Required
db_datareader Source database Read all tables for migration Yes
VIEW DATABASE STATE Source database Query DMVs for row counts, stats Yes
VIEW SERVER STATE Instance Monitor blocking, active sessions Recommended
db_owner on CDC DB Source database Enable / manage CDC for live sync CDC only
SQL Server Agent access Instance CDC capture jobs CDC only

ODBC Driver Check

Verify the correct ODBC driver is installed on the migration host:

-- PowerShell: list installed ODBC drivers
Get-OdbcDriver | Where-Object { $_.Name -like "*SQL*" } | Select-Object Name, Platform

Required: ODBC Driver 17 for SQL Server or ODBC Driver 18 for SQL Server.

Pre-Flight T-SQL Queries

Run these on the source instance before launching the tool:

-- 1. Verify connectivity and SQL Server version
SELECT SERVERPROPERTY('ProductVersion')  AS ProductVersion,
       SERVERPROPERTY('Edition')        AS Edition,
       SERVERPROPERTY('EngineEdition')   AS EngineEdition,
       @@SERVERNAME                       AS ServerName;
-- 2. Row counts for all user tables in the source database
SELECT
    s.name                          AS SchemaName,
    t.name                          AS TableName,
    p.rows                          AS RowCount,
    SUM(a.total_pages) * 8 / 1024    AS TotalSizeMB
FROM     sys.tables       t
JOIN     sys.schemas      s  ON s.schema_id  = t.schema_id
JOIN     sys.indexes      i  ON i.object_id  = t.object_id AND i.index_id <= 1
JOIN     sys.partitions   p  ON p.object_id  = i.object_id AND p.index_id  = i.index_id
JOIN     sys.allocation_units a ON a.container_id = p.partition_id
GROUP BY s.name, t.name, p.rows
ORDER BY TotalSizeMB DESC;
-- 3. Identify unsupported column types that need special handling
SELECT
    OBJECT_SCHEMA_NAME(c.object_id) AS SchemaName,
    OBJECT_NAME(c.object_id)        AS TableName,
    c.name                           AS ColumnName,
    tp.name                          AS DataType,
    c.max_length, c.is_nullable
FROM   sys.columns  c
JOIN   sys.types    tp ON tp.user_type_id = c.user_type_id
WHERE  tp.name IN ('hierarchyid', 'geometry', 'geography',
                    'sql_variant', 'xml', 'image',
                    'text', 'ntext')
ORDER BY SchemaName, TableName;
-- 4. Check for tables without a primary key (complicates id assignment)
SELECT
    s.name  AS SchemaName,
    t.name  AS TableName
FROM   sys.tables  t
JOIN   sys.schemas s ON s.schema_id = t.schema_id
WHERE  t.object_id NOT IN (
    SELECT parent_object_id
    FROM   sys.key_constraints
    WHERE  type = 'PK'
)
ORDER BY SchemaName, TableName;

Prerequisites — Target: Azure Cosmos DB

Python SDK Installation

The Migration Tool requires the Azure Cosmos DB Python SDK:

pip install azure-cosmos==4.7.0 azure-identity==1.16.0 pyodbc==5.1.0

Verify installation:

python -c "import azure.cosmos; print(azure.cosmos.__version__)"

Connection String Format

Retrieve from Azure Portal → Cosmos DB Account → Keys:

AccountEndpoint=https://<account-name>.documents.azure.com:443/;AccountKey=<primary-or-secondary-key>==;

For managed identity (recommended for production), use the endpoint URI only — no key required.

Azure Portal — Database Creation Steps

  1. Open the Azure Portal and navigate to your Cosmos DB account.
  2. Select Data ExplorerNew Container.
  3. Enter the Database id (create new or use existing).
  4. Enter the Container id (maps to source table name).
  5. Set the Partition key (e.g., /CustomerId).
  6. Choose throughput: Autoscale (recommended) or manual RU/s.
  7. Click OK. Container is ready within seconds.

RBAC Assignment (Azure CLI)

az cosmosdb sql role assignment create \
  --account-name  <cosmos-account> \
  --resource-group <rg-name> \
  --role-definition-name "Cosmos DB Built-in Data Contributor" \
  --principal-id  <managed-identity-object-id> \
  --scope          "/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.DocumentDB/databaseAccounts/<cosmos-account>"

Network Requirements

DirectionPortProtocolPurpose
Migration host → SQL Server1433TCPODBC / TDS connection
Migration host → Cosmos DB443HTTPSSDK data plane
Migration host → Azure AD443HTTPSToken acquisition (managed identity)

Step 1 — Configure Connections

Launch the Migration Tool and configure source and target connections. All credentials are stored encrypted in the local session and are never written to disk.

Source — SQL Server

sql-prod-01.acmecorp.internal
1433
AdventureWorks2019
SQL Server Authentication
migration_reader
••••••••••••

Target — Cosmos DB for NoSQL

https://he-cosmos-prod.documents.azure.com:443/
Managed Identity / Account Key
••••••••••••••••••••••••••••••••••
AdventureWorks_Cosmos
Autoscale — max 4,000 RU/s
Connection String URI Note: When using the full connection string format (AccountEndpoint=...;AccountKey=...;), paste the entire string into the Account Endpoint URI field. The tool will automatically parse both the endpoint and the key from the connection string.

Step 2 — Partition Key Assessment

The assessment engine analyzes each source table and recommends a partition key based on cardinality, query pattern sampling, and write distribution. Review and override recommendations before proceeding.

Partition Key Assessment — AdventureWorks Sample

Table Rows Recommended Partition Key Cardinality Confidence Notes
Sales.SalesOrderHeader 31,465 /CustomerId 847 distinct values High Aligns with common query filter
Sales.SalesOrderDetail 121,317 /SalesOrderId 31,465 distinct values High Co-locate with header via same partition
Production.Product 504 /ProductSubcategoryId 37 distinct values Medium Low cardinality; consider synthetic key
Person.Person 19,972 /EmailPromotion 3 distinct values Low — Review Hot partition risk; use /BusinessEntityId hash instead
HumanResources.Employee 290 /OrganizationLevel 4 distinct values Medium Contains hierarchyid column — will be skipped (see Known Issues)

Skipped / Unsupported Column Types

SQL Server TypeActionAlternative
hierarchyidSkipped by defaultExport as string via .ToString()
geometry / geographySkipped by defaultExport as WKT string or GeoJSON
sql_variantSkipped by defaultCast to nvarchar in source view
image / text / ntextSkipped by defaultCast to varbinary(max) / nvarchar(max)
xmlConverted to stringStored as escaped JSON string property
2 MB Document Size Limit: Cosmos DB enforces a hard 2 MB limit per document. Tables with varbinary(max), nvarchar(max), or large xml columns that routinely exceed this threshold must be split into a separate container or store binary data in Azure Blob Storage with a reference property in the document.

Step 3 — Table Mapping & Migration Options

Configure the mapping between source tables and target containers. Set the partition key, throughput, and per-table options before starting the migration run.

Migration Tool — Table Mapping
Source Table Target Container Partition Key Throughput Mode Status
Sales.SalesOrderHeader SalesOrderHeader /CustomerId 2,000 RU/s Bulk Ready
Sales.SalesOrderDetail SalesOrderDetail /SalesOrderId 4,000 RU/s Bulk Ready
Production.Product Product /ProductSubcategoryId 400 RU/s Bulk Ready
Person.Person Person /BusinessEntityId 1,000 RU/s Bulk Review PK
HumanResources.Employee Employee /OrganizationLevel 400 RU/s Bulk hierarchyid warn

Migration Options

OptionDefaultDescription
Batch Size1,000 documentsDocuments per upsert batch; reduce if hitting 429s
Parallel Workers4Concurrent threads per container
Retry on 429Enabled (5 retries)Exponential back-off on RU throttle
Checkpoint File./migration_checkpoint.jsonEnables resume on failure
Skip Unsupported TypesEnabledLog and skip; do not abort migration
Create ContainersEnabledAuto-create containers if not present
Upsert ModeEnabledOverwrite existing documents by id

Pre-Migration Checklist

Step 4 — Execute Migration

Click Start Migration to begin the bulk data movement. Monitor the live log panel. The migration is resumable via the checkpoint file if interrupted.

[2026-07-05 09:14:02] [INFO] Migration run started. Run ID: a3f9c2d7 [2026-07-05 09:14:03] [OK] Source connection established: sql-prod-01.acmecorp.internal\AdventureWorks2019 [2026-07-05 09:14:03] [OK] Target connection established: he-cosmos-prod.documents.azure.com [2026-07-05 09:14:04] [INFO] Creating container: SalesOrderHeader (partition: /CustomerId, 2000 RU/s) [2026-07-05 09:14:05] [OK] Container SalesOrderHeader created. [2026-07-05 09:14:05] [INFO] Migrating Sales.SalesOrderHeader → SalesOrderHeader [31,465 rows] [2026-07-05 09:14:12] [OK] Upserted 5,000 / 31,465 documents (RU consumed: 12,400) [2026-07-05 09:14:19] [OK] Upserted 10,000 / 31,465 documents (RU consumed: 24,800) [2026-07-05 09:14:26] [OK] Upserted 15,000 / 31,465 documents (RU consumed: 37,200) [2026-07-05 09:14:33] [OK] Upserted 20,000 / 31,465 documents (RU consumed: 49,600) [2026-07-05 09:14:40] [OK] Upserted 25,000 / 31,465 documents (RU consumed: 62,000) [2026-07-05 09:14:47] [OK] Upserted 30,000 / 31,465 documents (RU consumed: 74,400) [2026-07-05 09:14:49] [OK] SalesOrderHeader complete: 31,465 documents upserted. [2026-07-05 09:14:49] [INFO] Migrating HumanResources.Employee → Employee [290 rows] [2026-07-05 09:14:49] [WARN] Column OrganizationNode (hierarchyid) skipped for all 290 documents. See Known Issues #1. [2026-07-05 09:14:50] [OK] Employee complete: 290 documents upserted (1 column skipped per document). [2026-07-05 09:15:03] [INFO] Migrating Sales.SalesOrderDetail → SalesOrderDetail [121,317 rows] [2026-07-05 09:15:03] [INFO] RU consumption trend: 3.8 RU/doc average. [2026-07-05 09:16:22] [WARN] 429 TooManyRequests — retry 1/5 after 1,024 ms (partition: /SalesOrderId=18742) [2026-07-05 09:16:24] [OK] Retry succeeded. Continuing migration. [2026-07-05 09:18:44] [OK] SalesOrderDetail complete: 121,317 documents upserted. [2026-07-05 09:20:11] [OK] —— MIGRATION SUMMARY ——————————————————————————————————————————————— [2026-07-05 09:20:11] [OK] Tables migrated : 5 / 5 [2026-07-05 09:20:11] [OK] Total documents : 173,348 [2026-07-05 09:20:11] [OK] Total RU consumed: 658,724 [2026-07-05 09:20:11] [OK] Elapsed time : 00:06:09 [2026-07-05 09:20:11] [WARN] Skipped columns : 290 (hierarchyid in Employee.OrganizationNode) [2026-07-05 09:20:11] [OK] Checkpoint file : ./migration_checkpoint.json (retained for resume if needed)

Step 5 — Post-Migration Validation

Validate document counts and data integrity across all migrated containers. Use the Data Explorer queries, the Python SDK script, and T-SQL cross-validation.

Cosmos DB Data Explorer Queries

-- Count all documents in SalesOrderHeader
SELECT VALUE COUNT(1) FROM c

-- Spot-check a specific customer
SELECT * FROM c WHERE c.CustomerId = 29825

-- Verify field presence (no nulls on SalesOrderId)
SELECT VALUE COUNT(1) FROM c WHERE IS_NULL(c.SalesOrderId) = true

Expected Document Count Table

Container Source Row Count (T-SQL) Target Doc Count (Cosmos) Delta Status
SalesOrderHeader 31,465 31,465 0 PASS
SalesOrderDetail 121,317 121,317 0 PASS
Product 504 504 0 PASS
Person 19,972 19,972 0 PASS
Employee 290 290 0 PASS

Python SDK Verification Script

from azure.cosmos import CosmosClient

ENDPOINT = "https://he-cosmos-prod.documents.azure.com:443/"
KEY      = "<account-key>"
DB_NAME  = "AdventureWorks_Cosmos"

client = CosmosClient(ENDPOINT, credential=KEY)
db     = client.get_database_client(DB_NAME)

containers = ["SalesOrderHeader", "SalesOrderDetail",
              "Product", "Person", "Employee"]

for name in containers:
    container = db.get_container_client(name)
    count     = list(container.query_items(
        "SELECT VALUE COUNT(1) FROM c",
        enable_cross_partition_query=True
    ))[0]
    print(f"{name:30s}  documents: {count:,}")

T-SQL Cross-Validation (Source)

-- Run on source SQL Server to confirm expected row counts pre-cut-over
SELECT 'Sales.SalesOrderHeader'  AS TableName, COUNT(*) AS RowCount FROM Sales.SalesOrderHeader
UNION ALL
SELECT 'Sales.SalesOrderDetail'  ,             COUNT(*) FROM Sales.SalesOrderDetail
UNION ALL
SELECT 'Production.Product'      ,             COUNT(*) FROM Production.Product
UNION ALL
SELECT 'Person.Person'           ,             COUNT(*) FROM Person.Person
UNION ALL
SELECT 'HumanResources.Employee' ,             COUNT(*) FROM HumanResources.Employee;

Step 6 — Cut Over & Post-Migration

Index Policy Review

Cosmos DB indexes all properties by default. For large containers, a custom indexing policy reduces RU overhead on writes. Example policy for SalesOrderHeader:

{
  "indexingMode": "consistent",
  "automatic": true,
  "includedPaths": [
    { "path": "/CustomerId/?" },
    { "path": "/OrderDate/?" },
    { "path": "/Status/?" }
  ],
  "excludedPaths": [
    { "path": "/LineItems/*" },
    { "path": "/_etag/?" }
  ]
}

RU/s Scaling Steps

  1. During migration: set throughput to maximum needed (monitor CloudWatch / Azure Monitor).
  2. After migration and validation: scale down containers to steady-state RU/s.
  3. Enable Autoscale for variable workloads to avoid manual scaling.
  4. Set an alert if normalized RU consumption exceeds 80% for >5 minutes.

Change Feed Consumer (Application Cut-Over)

To detect and react to new writes after cut-over, configure a Change Feed processor:

from azure.cosmos.aio import CosmosClient
from azure.cosmos import PartitionKey

async def process_changes(docs):
    for doc in docs:
        print(f"Changed document id={doc['id']}, partition={doc.get('CustomerId')}")

# Attach processor to SalesOrderHeader container
# See: https://aka.ms/cosmos-change-feed-processor
🚨
Soft-Delete Warning: Cosmos DB Change Feed does NOT emit events for hard deletes. If your application relies on detecting deleted records, implement a soft-delete pattern (set a deleted: true flag and a TTL) before cut-over. Hard deletes are invisible to Change Feed consumers.

Decommission Checklist

Live CDC Sync (Change Data Capture)

For near-zero downtime migrations, enable SQL Server CDC on the source database to capture ongoing changes while the initial full-load runs. After full-load completes, replay CDC events into Cosmos DB before cutting over application traffic.

Enable CDC on Source Database

-- Step 1: Enable CDC on the database
USE AdventureWorks2019;
EXEC sys.sp_cdc_enable_db;

-- Step 2: Enable CDC on target tables
EXEC sys.sp_cdc_enable_table
    @source_schema   = N'Sales',
    @source_name     = N'SalesOrderHeader',
    @role_name       = NULL,
    @supports_net_changes = 1;

EXEC sys.sp_cdc_enable_table
    @source_schema   = N'Sales',
    @source_name     = N'SalesOrderDetail',
    @role_name       = NULL,
    @supports_net_changes = 1;

-- Step 3: Verify CDC is enabled
SELECT name, is_cdc_enabled
FROM   sys.databases
WHERE  name = N'AdventureWorks2019';

Full-Load-First Sequence

  1. Record the current CDC LSN (Log Sequence Number) before starting the full load.
  2. Execute the full-load migration (Step 4) while CDC continues capturing changes.
  3. After full-load completes, replay CDC change events from the saved LSN forward.
  4. Drain the CDC queue until lag reaches near-zero.
  5. Pause writes on the source (maintenance window), apply final CDC events, cut over.
-- Capture the starting LSN before full load begins
DECLARE @start_lsn binary(10);
SET @start_lsn = sys.fn_cdc_get_min_lsn(N'Sales_SalesOrderHeader');
SELECT sys.fn_cdc_map_lsn_to_time(@start_lsn) AS StartLSNTime, @start_lsn AS StartLSN;

CDC Operation Mapping

CDC __$operation Operation Cosmos DB Action Notes
1DELETEDelete document by id + partition keyRequires full PK value
2INSERTUpsert new documentAssign id from PK
3UPDATE (before)Ignore (use after-image)Pre-update snapshot
4UPDATE (after)Upsert updated documentFull row replacement
🚨
Change Feed Does Not Propagate Deletes: Cosmos DB Change Feed only emits insert and update events. If you use Cosmos DB Change Feed on the target side for downstream consumers, they will never see deletes. Handle delete propagation explicitly via soft-delete or a separate delete-event queue.

CDC Monitoring Queries

-- Monitor CDC capture latency
SELECT
    ct.capture_instance,
    ct.source_schema,
    ct.source_table,
    sys.fn_cdc_map_lsn_to_time(ct.start_lsn) AS CaptureStartTime,
    js.latency                                 AS LatencySeconds,
    js.tran_count                              AS UnprocessedTrans
FROM   cdc.change_tables          ct
JOIN   cdc.dm_cdc_log_scan_sessions js
    ON js.session_id = (SELECT MAX(session_id) FROM cdc.dm_cdc_log_scan_sessions)
ORDER BY ct.source_schema, ct.source_table;

Type Conversions Reference

The Migration Tool applies the following type mappings when transforming SQL Server rows to JSON documents.

SQL Server Type JSON / Cosmos DB Type Notes
int, bigint, smallint, tinyintNumber (integer)Direct mapping
decimal, numericNumber (decimal string)Stored as string to preserve precision; cast in application layer
float, realNumber (float)Potential precision loss for very large values
money, smallmoneyNumber (decimal string)Same as decimal — precision-safe string form
bitBoolean1true, 0false
char, varcharStringTrimmed of trailing spaces
nchar, nvarcharStringUnicode preserved
text, ntextString (if < 2 MB)Deprecated types; cast to nvarchar(max) first recommended
dateString (ISO 8601: YYYY-MM-DD)
datetime, datetime2String (ISO 8601: YYYY-MM-DDTHH:mm:ss.fffffffZ)UTC normalized
datetimeoffsetString (ISO 8601 with offset)Offset preserved in string
timeString (HH:mm:ss.fffffff)
uniqueidentifierString (lowercase UUID)Used as id when it is the PK
varbinary, binary, imageString (Base64)Warn if > 1 MB — 2 MB doc limit risk
xmlString (escaped XML)Not parsed to nested JSON by default
hierarchyidSkipped (default) / StringEnable Convert hierarchyid to string option to emit /1/2/3/ form
geometry / geographySkipped (default) / GeoJSONEnable Convert spatial to GeoJSON option
sql_variantSkippedNo safe automatic conversion; pre-cast in source view

Sample Output — SalesOrderHeader Document

{
  "id":              "75123",
  "SalesOrderId":    75123,
  "CustomerId":      29825,
  "OrderDate":       "2014-06-01T00:00:00.0000000Z",
  "DueDate":         "2014-06-13T00:00:00.0000000Z",
  "ShipDate":        "2014-06-08T00:00:00.0000000Z",
  "Status":          5,
  "OnlineOrderFlag": false,
  "SalesOrderNumber":"SO75123",
  "PurchaseOrderNumber": "PO18850127500",
  "AccountNumber":   "10-4020-000676",
  "SubTotal":        "880.3484",
  "TaxAmt":          "70.4279",
  "Freight":         "22.0087",
  "TotalDue":        "972.7850",
  "Comment":         null,
  "rowguid":         "79b65321-39ca-4115-9cba-8fe0903f12e6",
  "ModifiedDate":    "2014-06-08T00:00:00.0000000Z",
  "_migrationRunId": "a3f9c2d7",
  "_migratedAt":     "2026-07-05T09:14:49Z"
}

Known Issues & Limitations

🔴
Issue #1 — hierarchyid Not Natively Supported: The hierarchyid CLR type cannot be serialized to JSON without explicit conversion. By default, columns of this type are skipped. Enable the Convert hierarchyid to string option to emit the string representation (e.g., /1/2/3/), but note that hierarchyid-based queries and methods will not work in Cosmos DB — redesign the data model for tree traversal using parent-id references or materialized paths.
🔴
Issue #2 — 2 MB Document Size Hard Limit: Cosmos DB enforces a strict 2 MB limit per document. Documents exceeding this limit are rejected with HTTP 413. Tables with large varbinary(max) or nvarchar(max) columns must be split: store large binary payloads in Azure Blob Storage and embed a reference URI in the Cosmos DB document. Validate document sizes in the assessment phase.
🟡
Issue #3 — Partition Key Immutability: The partition key is set at container creation and cannot be changed. To change a partition key, you must create a new container, re-migrate all data, and update application code. This is a full re-migration — plan accordingly.
🟡
Issue #4 — No Multi-Document ACID Transactions Across Partitions: Cosmos DB transactions are scoped to a single logical partition. SQL Server transactions spanning multiple tables or rows in different partitions have no direct equivalent. Redesign workflows to use idempotent upserts and compensating transactions.
🟡
Issue #5 — Decimal Precision Representation: decimal and money values are stored as strings to preserve precision. Application code must parse these strings back to decimal/currency types. JSON number format cannot represent all SQL Server decimal precision without rounding.
🟢
Issue #6 — geometry / geography Columns: Spatial types are skipped by default. The Convert spatial to GeoJSON option emits RFC 7946 GeoJSON objects, but Cosmos DB for NoSQL does not support spatial indexing natively (use Azure Maps or a post-processing step for spatial queries).

Known Issues — Severity Summary

#IssueSeverityWorkaround Available
1 hierarchyid not serializable High Yes — convert to string or skip
2 2 MB document size limit High Yes — offload large blobs to Azure Blob Storage
3 Partition key immutability Medium Partial — requires full re-migration
4 No cross-partition ACID transactions Medium Yes — redesign with compensating transactions
5 Decimal precision as string Medium Yes — parse string in application layer
6 geometry / geography columns Low Yes — GeoJSON export option

UI Busy States & Checkpoint Behavior

During long-running migration operations, the tool enters a busy state and displays a progress indicator. The following behaviors apply:

OperationUI StateCancelableBehavior on Cancel
Connection Test Spinner, "Testing connection…" Yes Immediate cancel, no side effects
Assessment Scan Progress bar, table-by-table Yes Partial results shown; re-run to complete
Full Migration Run Log panel + per-table progress bars Yes (graceful) Completes current batch, writes checkpoint, stops
Validation Run Progress bar, container-by-container Yes Partial results flagged as incomplete
CDC Drain Log panel, LSN lag counter Yes (graceful) Stops after current event batch; LSN saved
💾
Checkpoint File: The migration checkpoint is written to ./migration_checkpoint.json after each successful batch. If the migration is interrupted (cancel, crash, network loss), re-launching the tool and clicking Resume Migration will read the checkpoint and continue from the last committed batch — no documents are re-written unnecessarily (upsert is idempotent). Delete the checkpoint file only after a successful, fully-validated migration.