Azure Cosmos DB Modes — Deep
Companion to the Azure Cosmos DB Specifics topic. Read that first for partition keys, RUs, change feed basics. This file goes deeper on API modes, consistency levels with their trade-offs, throughput modes (manual/autoscale/serverless/shared), multi-region writes, hierarchical partition keys, and bulk-mode .NET SDK patterns.
Key Points
- Five API modes: NoSQL (Core SQL — default and most featureful), MongoDB (4.0+ wire protocol), Cassandra (CQL v4), Gremlin (graph), Table (legacy compat). Use NoSQL unless you have a specific migration reason.
- Five consistency levels ordered by strength: Strong, Bounded Staleness, Session (default), Consistent Prefix, Eventual. Higher = more RUs + latency.
- Throughput modes: Manual (cheapest at flat load), Autoscale (10× elastic range), Serverless (≤5K RU/s, per-op billing), Shared-DB (many small containers).
- Hard limits: 2 MB per item, 20 GB per logical partition, 100 RU minimum per partition. Hot partitions = the silent perf killer.
- Indexing is automatic by default — opt out paths to reduce write RUs; composite indexes for
ORDER BYover multiple fields.
Concepts (deep dive)
The base Cosmos DB Specifics topic introduces partition keys, RUs, and the change feed at a working level. This file goes a layer down into the choices that shape Cosmos applications at scale: which API mode to pick when (and where each one quietly diverges from the original protocol), which consistency level trades off latency for which guarantee, which throughput model matches which traffic shape, and how physical partitions actually split under the hood (which determines whether a 20 GB logical-partition limit is going to bite you in 18 months). The patterns below assume you already know what a partition key is and are now picking the right one for production.
API modes — feature reality check
Use for Caveats
NoSQL (Core SQL) ✅ default New Cosmos workloads Most features, full Cosmos capability
MongoDB API Migrating Mongo apps Wire-compatible up to 6.0; API drift
Cassandra API Migrating C* / wide-column CQL v4; lacks many C* features
Gremlin API Graph traversal OK for medium graphs; not Neo4j-class
Table API Cheap upgrade from Storage Same limitations as Table Storage data model
Tables (perf + global SLA)
⚠️ Don't pick MongoDB API expecting parity with native MongoDB. API drift is real: some operators, aggregation features, and indexes are unsupported or behave differently. Test your workload before committing.
💡 The Vector search announcement (2024–2025), hierarchical partition keys, and full-text search roll out on NoSQL API first. Other APIs lag.
Consistency levels — the famous five
Stronger / slower / costlier ◄──────────────────────────► Looser / faster / cheaper
Strong ─ Bounded Staleness ─ Session (default) ─ Consistent Prefix ─ Eventual
| Level | Reads guarantee | Cost (read RUs) | Multi-region writes? |
|---|---|---|---|
| Strong | Linearizable; latest committed write everywhere | 2× | ❌ Single write region only |
| Bounded Staleness | Behind by ≤K writes or ≤T time | 2× | ✅ |
| Session | Read-your-own-writes within a session | 1× (default) | ✅ |
| Consistent Prefix | Updates seen in order, but stale | 1× | ✅ |
| Eventual | Eventually converges; no order guarantee | 1× | ✅ |
Strong disables multi-region writes — that's the price. For most apps, Session is correct: each user sees their own writes, low latency, multi-region writes available.
✅ Default to Session. Step up only if business logic demands it (financial transactions across regions: bounded staleness with a tight K).
Partition design — beyond the basics
Logical partition (one PK value)
├─ All items sharing the PK
├─ HARD LIMIT: 20 GB total
├─ HARD LIMIT: queries scoped to single PK = single-partition (cheap)
│
Physical partition (Cosmos-managed split)
├─ Holds many logical partitions
├─ ~50 GB capacity, 10K RU/s baseline
└─ Split automatically when needed
- Item size limit: 2 MB.
- Partition key path at container creation; immutable afterwards.
- Cosmos splits physical partitions transparently as data grows.
- A logical partition that exceeds 20 GB is a fatal design error — you can't migrate easily; you have to rebuild.
Hot partition — the silent perf killer
RU/s allocated: 10,000
┌──────────────┬──────────────┬──────────────┐
│ partition A │ partition B │ partition C │
│ (3,333 RU/s) │ (3,333 RU/s) │ (3,333 RU/s) │
└──────────────┴──────────────┴──────────────┘
▲
│ all "current day" writes here
│ → throttled at 3,333 RU/s
│ → 429 errors despite 10K provisioned
RU/s are divided across physical partitions. A skewed key burns one partition's quota while others sit idle. Solutions: - High-cardinality key (/userId). - Synthetic key (/userId-yyyymm) — combines locality + spread. - Hierarchical partition keys (newer feature).
Hierarchical partition keys (newer)
var props = new ContainerProperties("orders", new[] { "/tenantId", "/userId" });
await db.CreateContainerAsync(props, throughput: 10_000);
- Up to 3 levels.
- Queries can filter at any prefix level (
/tenantIdonly) without cross-partition scan. - Subpartitions can each go to 20 GB → effective per-tenant ceiling raised dramatically.
✅ Excellent for multi-tenant schemas where one tenant occasionally goes huge.
Throughput modes — pick the right model
Mode Min RU/s Max RU/s Billing Best for
────────── ──────── ───────── ───────────────── ─────────────────
Manual 400/cont unlimited per-RU/sec/hr Flat predictable load
Autoscale 400 max 10× scale per-RU/sec/hr (max) Variable load, peaks
Serverless n/a 5,000 RU/s per-operation Sporadic, dev/test
Shared-DB 400/db pooled db-level RU/s split Many small containers
Manual provisioned
Cheapest at high steady RPS. You manage scaling. Pay 24/7 for the provisioned ceiling.
Autoscale
Scales between 10% and 100% of max (e.g., 400–4,000). Pays for max if any 1-second tick uses it; otherwise scales down. Autoscale at idle = ~10% of provisioned cost. Good default for variable load.
Serverless
- Per-operation RU billing, no provisioned floor.
- 5,000 RU/s ceiling per container.
- No global distribution (single region).
- ✅ Dev/test, low-volume, sporadic apps.
- ❌ Production scale, multi-region.
Shared-throughput database
var props = ThroughputProperties.CreateManualThroughput(4000);
await client.CreateDatabaseAsync("mydb", props);
// All containers share 4000 RU/s (with up to 25 unprovisioned containers)
Cheap when you have many small containers. RUs split dynamically. Don't mix with one hot container — it'll starve the rest.
RU pricing — what to track
var resp = await container.ReadItemAsync<Order>(id, new PartitionKey(pk));
Console.WriteLine($"RU: {resp.RequestCharge}");
Track in OpenTelemetry: - RU per query type (point read vs cross-partition). - Hot operations consuming >50 RU. - 429 throttle rate — alert if >0.5%. - Per-partition consumption (via Azure Monitor).
Change feed — pull vs push
Pull model (advanced)
using var iterator = container.GetChangeFeedIterator<Order>(
ChangeFeedStartFrom.Beginning(), ChangeFeedMode.Incremental);
while (iterator.HasMoreResults)
{
var page = await iterator.ReadNextAsync();
foreach (var change in page) Process(change);
}
Manual control over cursors. Useful for one-shot rebuilds.
Push model (Change Feed Processor)
var processor = container
.GetChangeFeedProcessorBuilder<Order>(
processorName: "order-projector",
onChangesDelegate: async (changes, ct) =>
{
foreach (var c in changes) await projection.ApplyAsync(c, ct);
})
.WithInstanceName(Environment.MachineName)
.WithLeaseContainer(leaseContainer)
.Build();
await processor.StartAsync();
- Lease container distributes work across instances (each lease = one partition).
- Auto-checkpoints, auto-recovers.
- Ideal for materialized views, search index updates, integration to Service Bus / Event Hubs.
Azure Functions Cosmos trigger
[Function(nameof(OrderProjector))]
public async Task Run(
[CosmosDBTrigger(
databaseName: "shop",
containerName: "orders",
Connection = "Cosmos",
LeaseContainerName = "leases")] IReadOnlyList<Order> changes)
{
foreach (var c in changes) await Project(c);
}
Serverless materialized-view rebuilding. Just write the projector; Functions runtime owns leases + scaling.
Indexing — automatic but tunable
{
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [{ "path": "/*" }],
"excludedPaths": [{ "path": "/largeMetadata/*" }],
"compositeIndexes": [
[
{ "path": "/customerId", "order": "ascending" },
{ "path": "/createdAt", "order": "descending" }
]
]
}
- Consistent = updated synchronously with writes (default).
- Lazy = async; queries may miss recent docs (rarely used).
- Composite indexes required for multi-field
ORDER BYand some range/equality combos. - Spatial / range indexes on geospatial paths.
✅ Excluding rarely-queried fields (largeMetadata) reduces write RUs significantly on large items.
Multi-region writes (multi-master)
var client = new CosmosClient(uri, credential, new CosmosClientOptions
{
ApplicationRegion = Regions.EastUS,
ConnectionMode = ConnectionMode.Direct,
ConsistencyLevel = ConsistencyLevel.Session
});
- Account configured with multi-region writes = on.
- Each region writes locally; replication async.
- Conflicts on the same item, same instant resolved by:
- Last-Writer-Wins (default; uses
_ts). - Custom merge via stored procedure (rare).
- Conflict feed captures conflicts for audit.
⚠️ Strong consistency disables multi-region writes. Pick one.
Composite indexes — concrete example
Query: SELECT * FROM c WHERE c.tenantId = @t ORDER BY c.createdAt DESC, c.priority
Without composite index → fails / scans. With:
[
{ "path": "/tenantId", "order": "ascending" },
{ "path": "/createdAt", "order": "descending" },
{ "path": "/priority", "order": "ascending" }
]
Composite indexes have a fixed order. Plan for your top queries.
Bulk mode (.NET SDK v3)
var client = new CosmosClient(uri, credential, new CosmosClientOptions
{
AllowBulkExecution = true,
ConnectionMode = ConnectionMode.Direct
});
var tasks = new List<Task>();
foreach (var order in batch)
tasks.Add(container.CreateItemAsync(order, new PartitionKey(order.CustomerId)));
await Task.WhenAll(tasks);
- SDK groups concurrent operations into batches per partition.
- 10–100× throughput vs sequential.
- Use for ingest, migration, change-feed projection.
Server-side: stored procs / UDFs / triggers
JavaScript-based, scoped to a single logical partition.
⚠️ Avoid for new code. Reasons: - Hard to test / version. - Atomic only within one partition. - Replaced by change feed + app-side logic for most patterns. - UDFs in queries can hide RU cost.
Exceptions: tight performance-critical atomic upserts within a partition.
Time-to-Live (TTL)
await container.ReplaceContainerAsync(new ContainerProperties
{
Id = "sessions",
PartitionKeyPath = "/userId",
DefaultTimeToLive = 3600
});
Per-container default; per-item override ({ "ttl": 60 }). Set -1 to disable for an item that should never expire. Excellent for sessions, caches, ephemeral data.
Vector search (Cosmos NoSQL, GA 2024–2025)
var query = new QueryDefinition(
"SELECT TOP 10 c.id, VectorDistance(c.embedding, @q) AS score " +
"FROM c ORDER BY VectorDistance(c.embedding, @q)")
.WithParameter("@q", queryEmbedding);
Native vector indexing alongside operational data. RAG-ready in a single store. See Cosmos DB vector store.
Diagnostic data
var resp = await container.ReadItemAsync<Order>(...);
var diag = resp.Diagnostics.ToString();
// JSON: latency breakdown, retried regions, RU charge per call
Log on slow ops; this is the trail for senior debugging.
How it works under the hood
- A Cosmos account is a logical control plane mapping to a partition map of physical partitions distributed across regions.
- Each physical partition runs a replica set (4 replicas, quorum-based) for HA — even with single-region.
- Direct mode opens TCP connections to backend nodes (lower latency, fewer RUs); Gateway mode routes via HTTPS through a control-plane gateway (firewall-friendly).
- Session token is a vector clock per partition; the SDK tracks it client-side to enforce read-your-writes.
- Multi-region writes use anti-entropy replication; conflicts surface to the conflict feed.
- RU charging sums I/O, indexing cost, query parsing, and result set size.
Code: correct vs wrong
❌ Wrong: Strong consistency for multi-region app
Disables multi-region writes; doubles read RU; high latency.
✅ Correct: Session (default)
Multi-region writes available; read-your-own-writes preserved.
❌ Wrong: cross-partition queries everywhere
Fan-out across all partitions; expensive.
✅ Correct: include PK or design key for query
❌ Wrong: 100K RU/s autoscale max defensively
Runaway query consumes ceiling for 1 hour = enormous bill.
✅ Correct: tight max + alerting on 80% sustained
ThroughputProperties.CreateAutoscaleThroughput(4000);
// Azure Monitor alert on 80% RU sustained for 10 min
❌ Wrong: low-cardinality partition key
✅ Correct: high-cardinality + locality
Design patterns for this topic
Pattern 1 — "Session consistency by default"
- Intent: read-your-writes without strong-consistency cost; multi-region writes available.
Pattern 2 — "Hierarchical partition keys for multi-tenant"
- Intent: scale tenants past the 20 GB logical partition ceiling cleanly.
Pattern 3 — "Change feed → materialized view"
- Intent: event-sourced read models; rebuilds at will.
Pattern 4 — "Bulk mode for ingestion"
- Intent: 10-100× throughput vs sequential inserts.
Pattern 5 — "Composite indexes for ORDER BY"
- Intent: multi-field sorted queries without full scan.
Pattern 6 — "Autoscale + tight max for variable load"
- Intent: scale to peak; pay 10% at idle; cap blast radius.
Pros & cons / trade-offs
| Aspect | Pros | Cons |
|---|---|---|
| NoSQL API | Most features, vector, hierarchical PK | Cosmos lock-in |
| Mongo API | Migrate Mongo apps | API drift, lags features |
| Strong consistency | Linearizable | No multi-region writes; 2× RU |
| Session | Read-your-writes | Default; rarely wrong |
| Manual RU | Cheapest at steady RPS | Manage scaling |
| Autoscale | Elastic, idle savings | Ceiling cost on 100% |
| Serverless | Pay per op | 5K RU/s cap, single region |
| Bulk mode | Massive throughput | Higher transient RU |
| Multi-region writes | Local writes, HA | Conflict resolution complexity |
| Hierarchical PK | Multi-tenant scale | NoSQL API only |
| Stored procs | Atomic per partition | Hard to test, avoid for new code |
When to use / when to avoid
- Use NoSQL API for any new Cosmos work.
- Use Session consistency unless business specifically requires stronger.
- Use autoscale for variable workloads, manual for flat high-RPS.
- Use serverless for sporadic / dev / test.
- Use hierarchical partition keys for multi-tenant SaaS.
- Use change feed processor / Functions trigger for materialized views.
- Avoid Mongo API expecting full Mongo parity.
- Avoid Strong consistency unless specific need; cost + multi-region trade-off.
- Avoid cross-partition queries in hot paths.
- Avoid stored procs for new code.
Interview Q&A
Q1. Five APIs and which to default to? NoSQL (default), MongoDB, Cassandra, Gremlin, Table. NoSQL has all features.
Q2. Five consistency levels? Strong, Bounded Staleness, Session (default), Consistent Prefix, Eventual.
Q3. Why Session as default? Read-your-writes guarantee + low latency + multi-region writes available.
Q4. Strong consistency limitation? Disables multi-region writes; one write region only.
Q5. Throughput modes? Manual (flat), Autoscale (10× elastic), Serverless (≤5K RU/s, per-op), Shared-DB (pooled).
Q6. Hot partition? RU exhaustion on one logical partition while others idle. Caused by low-cardinality or skewed key.
Q7. Hierarchical partition keys? Up to 3 levels; queries filter at any prefix; raises per-tenant ceiling beyond 20 GB.
Q8. Change feed processor vs Azure Functions trigger? Processor: full control, custom hosting. Functions: serverless, auto-scale, simple deploy.
Q9. Composite indexes? For multi-field ORDER BY and range+equality combinations; default indexing doesn't cover.
Q10. Bulk mode? SDK option that batches concurrent ops per partition; massive ingest throughput.
Q11. Direct vs Gateway connection mode? Direct: TCP to backends, lower latency, fewer RUs. Gateway: HTTPS via gateway, firewall-friendly.
Q12. Item size + logical partition limits? 2 MB per item; 20 GB per logical partition (hard).
Q13. Conflict resolution in multi-region writes? Last-Writer-Wins by _ts (default); custom merge via stored proc (rare).
Q14. When to use serverless? Sporadic / dev / test workloads under 5K RU/s and single region.
Gotchas / common mistakes
- ⚠️ Wrong partition key chosen at design time — immutable; rebuild required.
- ⚠️ 20 GB logical partition limit hit silently — Cosmos rejects writes; no graceful split.
- ⚠️ Strong consistency picked "to be safe" — costs RUs and disables multi-region writes.
- ⚠️ Autoscale max set 100K RU/s defensively — runaway query = giant bill.
- ⚠️ Cross-partition queries in hot path — fan out, RU consumption explodes.
- ⚠️ No composite index for ORDER BY — query fails or full scan.
- ⚠️ Mongo API parity assumed — drift on operators, indexes, aggregations.
- ⚠️ Default index everything for large items — wasteful write RU; opt out unused fields.
- ⚠️ No diagnostics logging — debugging slow ops is impossible without
Diagnostics. - ⚠️ Stored procs for new business logic — fragile, hard to test; use change feed.