MongoDB to Cosmos DB for MongoDB (RU-Based) Migration Guide¶
Comparative positioning note
This document is written from the perspective of Microsoft Azure, Cloud Scale Analytics, and CSA Loom. Any description of third-party or competing products, services, pricing, or capabilities is derived from publicly available documentation and sources believed accurate at the time of writing, and is provided for general comparison only. We do not claim expertise in, or authority over, any non-Microsoft product or service; the respective vendor's official documentation is the authoritative source for their offerings, which may change over time. Nothing here is intended to disparage any vendor — where a competing product has genuine advantages, we aim to note them honestly. Verify all third-party details against the vendor's current official documentation before making decisions.
Audience: Platform architects, data engineers, and application developers migrating from MongoDB to Azure Cosmos DB for MongoDB with the request-unit (RU) throughput model.
Overview¶
Cosmos DB for MongoDB (RU-based) is a globally distributed, multi-model database service that uses request units (RU/s) as its throughput currency. Unlike vCore (which mirrors a traditional cluster architecture), the RU model abstracts compute and storage into a throughput-provisioned service with automatic partitioning, global distribution, and analytical store capabilities. This model is best suited for applications that need planetary-scale distribution, event-driven architectures via change feed, or zero-ETL HTAP through analytical store.
1. Understanding request units¶
A request unit (RU) represents the normalized cost of a database operation. Every operation -- read, write, query, or aggregation -- consumes RUs based on:
- Document size -- larger documents consume more RUs per operation.
- Index overhead -- more indexed properties increase write RU cost.
- Query complexity -- simple point reads cost 1 RU per 1 KB document. Scans, sorts, and aggregations cost more.
- Consistency level -- strong consistency reads cost 2x eventual consistency reads.
RU cost examples¶
| Operation | Approximate RU cost |
|---|---|
Point read (1 KB document, by _id + partition key) | 1 RU |
| Point read (10 KB document) | ~3 RU |
| Insert (1 KB document) | ~6 RU |
| Insert (10 KB document) | ~20 RU |
| Replace (1 KB document) | ~10 RU |
| Query returning 5 documents (1 KB each, indexed filter) | ~5 RU |
| Aggregation (scan 1,000 documents, return 10) | ~50--200 RU |
| Cross-partition query (fan-out) | 5--50x single-partition |
Estimating total RU/s requirement¶
Example: An application doing 500 point reads/sec (1 RU each), 100 inserts/sec (6 RU each), and 50 queries/sec (20 RU each) needs:
With autoscale (10x range), provision 2,100 RU/s minimum, autoscaling to 21,000 RU/s for peaks.
2. Throughput provisioning models¶
Manual throughput¶
- Fixed RU/s provisioned at container or database level.
- Minimum: 400 RU/s (or 100 RU/s with shared throughput database).
- Scales in increments of 100 RU/s.
- Best for: steady-state workloads with predictable traffic.
Autoscale throughput¶
- Provisions a maximum RU/s; Cosmos DB scales between 10% of max and max.
- Billed at the highest RU/s reached in each hour.
- Best for: variable workloads, batch + interactive mixed patterns.
- Example: autoscale max 10,000 RU/s. If traffic uses only 2,000 RU/s, billed at 2,000.
Serverless¶
- No provisioned throughput. Billed per RU consumed.
- Maximum 5,000 RU/s burst.
- Best for: dev/test, low-traffic applications, event-driven microservices.
- Limitation: single-region only, no geo-replication, no analytical store.
Choosing the right model¶
| Workload pattern | Recommended model | Why |
|---|---|---|
| Steady 24/7 traffic | Manual (with reserved capacity discount) | Lowest cost for predictable load |
| Business hours peak, quiet nights | Autoscale | Scales down automatically during off-hours |
| Unpredictable bursts | Autoscale | Handles 10x traffic spikes without throttling |
| Dev/test | Serverless | Zero cost when idle |
| Batch processing (nightly ETL) | Autoscale (with programmatic max increase) | Scale up for batch, scale down after |
3. Partition key design¶
The partition key is the single most important design decision for RU-based Cosmos DB. It is immutable after container creation.
What a partition key does¶
- Determines how documents are distributed across physical partitions.
- Scopes transactions (multi-document transactions only within a single partition key value).
- Affects query performance (single-partition queries are cheap; cross-partition queries fan out).
- Determines throughput distribution (each physical partition has a max of 10,000 RU/s).
Partition key selection criteria¶
| Criterion | Good partition key | Bad partition key |
|---|---|---|
| Cardinality | High (many distinct values) | Low (few values, e.g., status with 3 values) |
| Distribution | Even (similar document counts per value) | Skewed (one value has 80% of documents) |
| Query affinity | Most queries filter by this field | Queries rarely filter by this field |
| Write distribution | Writes spread across many values | All writes go to one value (hot partition) |
Common patterns¶
| Use case | Recommended partition key | Rationale |
|---|---|---|
| User profiles | /userId | High cardinality, most queries filter by user |
| Orders | /customerId | Queries by customer; transactions scope to customer |
| IoT telemetry | /deviceId | Even distribution across devices |
| Multi-tenant SaaS | /tenantId | Isolates tenant data; tenant-scoped queries |
| Catalog / products | /categoryId | Moderate cardinality; queries by category |
| Logs / events | /partitionDate (YYYY-MM-DD) | Time-based distribution; avoid hot "today" partition by adding a suffix |
Hierarchical partition keys¶
For scenarios needing multi-level distribution, Cosmos DB supports hierarchical partition keys (up to 3 levels):
This allows fine-grained distribution while still supporting efficient queries at any level of the hierarchy.
Anti-patterns¶
_idas partition key -- ObjectId provides high cardinality but no query affinity. Every query becomes cross-partition.- Timestamp as sole partition key -- creates hot partitions at the current time boundary.
- Status fields -- low cardinality (e.g.,
active/inactive) creates severe skew. - Overly specific keys -- if every document has a unique partition key, transactions become impossible.
4. Capacity planning from MongoDB metrics¶
Step 1: Collect MongoDB metrics¶
# Current operations per second
mongostat --uri="mongodb+srv://..." --rowcount=60
# Document sizes
mongosh --eval '
db.orders.aggregate([
{ $sample: { size: 1000 } },
{ $project: { size: { $bsonSize: "$$ROOT" } } },
{ $group: {
_id: null,
avgSize: { $avg: "$size" },
maxSize: { $max: "$size" },
p95Size: { $percentile: { input: "$size", p: [0.95], method: "approximate" } }
}}
])
'
# Operations breakdown
mongosh --eval 'db.serverStatus().opcounters'
Step 2: Calculate RU requirements¶
Map MongoDB operations to RU costs:
MongoDB op/s → Cosmos DB RU/s
═══════════════════════════════
Reads: getmore + query → Point reads (1 RU/KB) or queries (5-200 RU)
Inserts: insert → Insert (5-20 RU depending on size + indexes)
Updates: update → Replace (10-30 RU) or partial update (5-15 RU)
Deletes: delete → Delete (5-10 RU)
Step 3: Add headroom¶
- Add 20% headroom for indexing overhead.
- Add 30% headroom for query plan variations.
- If using autoscale, set max at 3--5x the calculated steady-state.
5. Indexing policy design¶
Unlike MongoDB, where you explicitly create indexes, RU-based Cosmos DB uses a declarative indexing policy. By default, all properties are indexed, which maximizes query flexibility but increases write RU cost.
Default indexing policy (all properties indexed)¶
{
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [{ "path": "/*" }],
"excludedPaths": [{ "path": "/\"_etag\"/?" }]
}
Optimized indexing policy (targeted)¶
{
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [
{ "path": "/customerId/?" },
{ "path": "/orderDate/?" },
{ "path": "/status/?" },
{ "path": "/total/?" }
],
"excludedPaths": [{ "path": "/*" }],
"compositeIndexes": [
[
{ "path": "/customerId", "order": "ascending" },
{ "path": "/orderDate", "order": "descending" }
]
]
}
Impact: Targeted indexing reduces write RU cost by 20--50% compared to the default "index everything" policy. Only index properties that appear in query filters, sort clauses, or range predicates.
6. Migration execution steps¶
Step 1: Provision Cosmos DB account¶
# Create account with MongoDB API
az cosmosdb create \
--resource-group rg-data-platform \
--name my-cosmos-account \
--kind MongoDB \
--server-version 7.0 \
--default-consistency-level Session \
--locations regionName=eastus failoverPriority=0 isZoneRedundant=true \
--locations regionName=westus failoverPriority=1 isZoneRedundant=false \
--enable-analytical-storage true \
--backup-policy-type Continuous
Step 2: Create database and containers with partition keys¶
# Create database with shared throughput
az cosmosdb mongodb database create \
--resource-group rg-data-platform \
--account-name my-cosmos-account \
--name mydb \
--throughput 4000
# Create container with partition key
az cosmosdb mongodb collection create \
--resource-group rg-data-platform \
--account-name my-cosmos-account \
--database-name mydb \
--name orders \
--shard "customerId" \
--analytical-storage-ttl -1 \
--throughput 4000
Step 3: Configure indexing policy¶
Use the Azure Portal or Azure CLI to set a targeted indexing policy on each container. See Section 5 above.
Step 4: Migrate data¶
See Data Migration Guide for detailed options. For RU-based, Azure DMS with online CDC is the recommended path for production migrations.
Step 5: Enable analytical store¶
Analytical store is enabled per container (set at creation with --analytical-storage-ttl -1 for infinite retention). Once enabled, operational data automatically syncs to the column-oriented analytical store within approximately 2 minutes.
Step 6: Configure change feed consumers¶
// C# example: Azure Functions change feed trigger
[FunctionName("ProcessChangeFeed")]
public static void Run(
[CosmosDBTrigger(
databaseName: "mydb",
containerName: "orders",
Connection = "CosmosDBConnection",
LeaseContainerName = "leases",
CreateLeaseContainerIfNotExists = true)]
IReadOnlyList<Document> documents,
ILogger log)
{
foreach (var doc in documents)
{
log.LogInformation($"Change detected: {doc.Id}");
// Publish to Event Hubs for Fabric RTI
}
}
7. Autoscale configuration¶
Programmatic autoscale adjustment (for batch windows)¶
# Scale up before nightly batch
az cosmosdb mongodb collection throughput update \
--resource-group rg-data-platform \
--account-name my-cosmos-account \
--database-name mydb \
--name orders \
--max-throughput 50000
# Scale down after batch completes
az cosmosdb mongodb collection throughput update \
--resource-group rg-data-platform \
--account-name my-cosmos-account \
--database-name mydb \
--name orders \
--max-throughput 10000
Monitoring RU consumption¶
# Check current RU usage via Azure Monitor
az monitor metrics list \
--resource "/subscriptions/{sub}/resourceGroups/rg-data-platform/providers/Microsoft.DocumentDB/databaseAccounts/my-cosmos-account" \
--metric "TotalRequestUnits" \
--interval PT1M \
--aggregation Total
Monitor the NormalizedRUConsumption metric. If it consistently exceeds 70%, increase autoscale maximum. If consistently below 20%, reduce to save cost.
8. Rate limiting and retry strategy¶
When RU consumption exceeds provisioned throughput, Cosmos DB returns HTTP 429 (Too Many Requests) with a Retry-After header. MongoDB wire protocol translates this to error code 16500.
Driver retry configuration¶
// Node.js: configure retry for RU throttling
const client = new MongoClient(uri, {
retryWrites: true,
retryReads: true,
maxPoolSize: 50,
serverSelectionTimeoutMS: 30000,
socketTimeoutMS: 360000,
});
// Manually handle 16500 (rate limiting)
async function withRetry(fn, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (err) {
if (err.code === 16500 && i < maxRetries - 1) {
const retryAfterMs = err.errorLabels?.includes(
"RetryableWriteError",
)
? 100 * Math.pow(2, i)
: 1000;
await new Promise((resolve) =>
setTimeout(resolve, retryAfterMs),
);
continue;
}
throw err;
}
}
}
9. Global distribution setup¶
# Add regions for geo-replication
az cosmosdb update \
--resource-group rg-data-platform \
--name my-cosmos-account \
--locations regionName=eastus failoverPriority=0 isZoneRedundant=true \
--locations regionName=westus failoverPriority=1 isZoneRedundant=true \
--locations regionName=northeurope failoverPriority=2 isZoneRedundant=false
# Enable multi-region writes
az cosmosdb update \
--resource-group rg-data-platform \
--name my-cosmos-account \
--enable-multiple-write-locations true
Multi-region writes multiply the RU cost by the number of write regions. For a 3-region, multi-write deployment, every write consumes 3x the RU cost. Budget accordingly.
10. CSA-in-a-Box integration¶
The RU-based model unlocks two integration pathways unique to this deployment model:
Analytical store to Fabric¶
# Fabric Spark notebook: query Cosmos DB analytical store
df = spark.read \
.format("cosmos.olap") \
.option("spark.synapse.linkedService", "CosmosDb_mydb") \
.option("spark.cosmos.container", "orders") \
.load()
df.createOrReplaceTempView("orders_analytical")
# Run analytical queries without impacting operational workload
spark.sql("""
SELECT region, DATE(orderDate) as order_date, SUM(total) as revenue
FROM orders_analytical
WHERE orderDate >= '2026-01-01'
GROUP BY region, DATE(orderDate)
ORDER BY revenue DESC
""").show()
Change feed to Event Hubs to Fabric RTI¶
See examples/iot-streaming/ and csa_platform/data_activator/ for the full pattern. The change feed processor publishes events to Event Hubs; Fabric Real-Time Intelligence ingests them into a KQL database or Fabric lakehouse as Delta tables.
Related resources¶
- vCore Migration Guide
- Schema Migration
- Data Migration
- Application Migration
- Best Practices
- Migration Playbook
Maintainers: csa-inabox core team Last updated: 2026-04-30