Skip to content

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 BY over 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 ❌ Single write region only
Bounded Staleness Behind by ≤K writes or ≤T time
Session Read-your-own-writes within a session 1× (default)
Consistent Prefix Updates seen in order, but stale
Eventual Eventually converges; no order guarantee

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 (/tenantId only) 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

var throughput = ThroughputProperties.CreateManualThroughput(4000);

Cheapest at high steady RPS. You manage scaling. Pay 24/7 for the provisioned ceiling.

Autoscale

var throughput = ThroughputProperties.CreateAutoscaleThroughput(maxAutoscaleRUs: 4000);

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 BY and 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

new CosmosClientOptions { ConsistencyLevel = ConsistencyLevel.Strong };

Disables multi-region writes; doubles read RU; high latency.

✅ Correct: Session (default)

new CosmosClientOptions { ConsistencyLevel = ConsistencyLevel.Session };

Multi-region writes available; read-your-own-writes preserved.

❌ Wrong: cross-partition queries everywhere

SELECT * FROM c WHERE c.email = @e

Fan-out across all partitions; expensive.

✅ Correct: include PK or design key for query

SELECT * FROM c WHERE c.tenantId = @t AND c.email = @e

❌ 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

PartitionKey: /status   // 5 distinct values → hot partitions guaranteed

✅ Correct: high-cardinality + locality

PartitionKey: /tenantId-yyyymm
// Hierarchical option: ["/tenantId", "/userId"]

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.

Further reading