Problem Context

Azure Cosmos DB is a globally distributed, multi-model database that hides the hard parts of partitioning, replication, and consensus behind RU/s (Request Units per second). The 2025-2026 wave of changes matters: vector search GA in the NoSQL API, DiskANN indexes for billion-scale ANN, the vCore-based Cosmos DB for MongoDBas the "just want Mongo" option, and a serverless tier that finally makes proof-of-concept usage cost cents instead of dollars.

Cosmos earns its place when you need single-digit-ms reads worldwide, multi-region writes, or guaranteed throughput. It punishes you when you pick the wrong partition key, model relationally, or fan out reads across logical partitions. This guide is the playbook for getting the architecture right the first time, because RU bills compound mercilessly.

๐Ÿค” Sound familiar?
  • You picked id as your partition key and now everything is hot
  • You don't know the difference between Strong, Bounded Staleness, Session, Consistent Prefix, and Eventual
  • You're scared to read your RU bill
  • You're using SQL API but issuing cross-partition queries on every request

Pick the right partition key, the right consistency, and the right API โ€” the rest of Cosmos rewards you.

Concept Explanation

Cosmos uses logical partitions (rows sharing a partition-key value) inside physical partitions (each capped at 50 GB and 10k RU/s). Every operation costs RUs; reads of 1 KB by id โ‰ˆ 1 RU, writes โ‰ˆ 5 RU, cross-partition queries can be hundreds. The five consistency levels are a price/correctness slider:

  • Strong โ€” linearizable, only single-region or limited regions. Most expensive.
  • Bounded Staleness โ€” lag bounded by K versions or T seconds. Good multi-region default.
  • Session โ€” read-your-writes within a session token. Default and best for most apps.
  • Consistent Prefix โ€” reads see writes in order, never out of sequence.
  • Eventual โ€” cheapest, weakest. OK for analytics caches.

flowchart LR
    APP["App"] --> SDK["Cosmos SDK<br/>(direct mode TCP)"]
    SDK --> GW["Gateway / Direct"]
    GW --> P1["Physical Partition 1<br/>(replica set)"]
    GW --> P2["Physical Partition 2<br/>(replica set)"]
    GW --> PN["Physical Partition N<br/>(replica set)"]
    P1 --> R["Multi-region replication<br/>(consistency: Strong | Bounded | Session | Prefix | Eventual)"]

    style SDK fill:#0078D4,color:#fff,stroke:#005a9e
    style R fill:#16a34a,color:#fff,stroke:#15803d

Implementation

Step 1: Connect with managed identity, not keys

// Microsoft.Azure.Cosmos 3.45+ supports DefaultAzureCredential
var client = new CosmosClient(
    accountEndpoint: "https://my-acct.documents.azure.com:443/",
    tokenCredential: new DefaultAzureCredential(),
    new CosmosClientOptions
    {
        ConnectionMode      = ConnectionMode.Direct,    // ~3x faster
        ConsistencyLevel    = ConsistencyLevel.Session, // app-level default
        ApplicationRegion   = "West US 3",
        EnableContentResponseOnWrite = false            // saves RUs
    });

Step 2: Pick a partition key with high cardinality + even access

// GOOD: tenantId for a multi-tenant SaaS โ€” high cardinality, even traffic
var props = new ContainerProperties("orders", partitionKeyPath: "/tenantId");

// BAD: /status (only 5 values โ†’ 5 hot partitions)
// BAD: /id (each doc is its own partition โ†’ fan-out queries are death)
// BAD: /createdDate (today's partition is scorching, yesterday's is cold)

// Synthetic keys are a powerful escape hatch:
// partitionKey = "tenant42:2026-04" โ†’ high cardinality + locality of recent
await db.CreateContainerIfNotExistsAsync(props, throughput: 1000);

Step 3: Point reads, not queries

// 1 RU โ€” the cheapest possible read
var resp = await container.ReadItemAsync<Order>(
    id: orderId,
    partitionKey: new PartitionKey(tenantId));
Console.WriteLine($"RUs: {resp.RequestCharge}");

// In-partition query โ€” also cheap (single physical partition)
var q = container.GetItemQueryIterator<Order>(
    new QueryDefinition("SELECT * FROM c WHERE c.status='open'"),
    requestOptions: new QueryRequestOptions {
        PartitionKey = new PartitionKey(tenantId)
    });

// Cross-partition query โ€” avoid in hot paths
// "SELECT * FROM c WHERE c.email = @e" without partition key = fan-out

Step 4: Bulk writes for ingest

// Enable bulk mode in client options once
var bulkOpts = new CosmosClientOptions { AllowBulkExecution = true };

var tasks = orders.Select(o => container.CreateItemAsync(
    o, new PartitionKey(o.TenantId)));
await Task.WhenAll(tasks);   // SDK batches per partition under the hood

Step 5: Change feed = your event stream

// Run a long-lived processor in a worker
var processor = container
    .GetChangeFeedProcessorBuilder<Order>("orderProcessor",
        async (ChangeFeedProcessorContext ctx, IReadOnlyCollection<Order> changes,
               CancellationToken ct) =>
        {
            foreach (var o in changes) await EnqueueAsync(o, ct);
        })
    .WithInstanceName("worker-1")
    .WithLeaseContainer(leaseContainer)
    .Build();

await processor.StartAsync();
// Use it for outbox-style projections, search index updates, ML feature stores.

Step 6: Vector search (NoSQL API, GA)

// Container with vector embedding policy
var policy = new ContainerProperties("docs", "/tenantId")
{
    VectorEmbeddingPolicy = new VectorEmbeddingPolicy(new[] {
        new Embedding {
            Path = "/embedding", DataType = VectorDataType.Float32,
            DistanceFunction = DistanceFunction.Cosine, Dimensions = 1536
        }
    }),
    IndexingPolicy = new IndexingPolicy {
        VectorIndexes = { new VectorIndexPath {
            Path = "/embedding", Type = VectorIndexType.DiskANN
        } }
    }
};

// Hybrid query
var hits = container.GetItemQueryIterator<Doc>(
    new QueryDefinition("""
        SELECT TOP 10 c.id, c.title,
               VectorDistance(c.embedding, @q) AS sim
        FROM c
        WHERE c.tenantId = @tid
        ORDER BY VectorDistance(c.embedding, @q)
    """).WithParameter("@q", queryEmbedding)
        .WithParameter("@tid", tenantId));

Common Pitfalls

  1. Bad partition key.The single most expensive mistake. Aim for cardinality โ‰ซ number of physical partitions and roughly uniform access. Once chosen, you can't change it without copying the container.
  2. Cross-partition queries on hot paths. Each fan-out queries every physical partition. Add the partition key to the predicate or denormalize so reads are by-partition.
  3. Strong consistency by default. Halves write throughput and blocks multi-region writes. Use Session unless you have a specific reason.
  4. Reading RequestCharge only in dev. Log RU per logical operation in production; you'll find the 1000-RU query you didn't know existed.
  5. EnableContentResponseOnWrite = true (default).Returns the entire written document. Disable to halve write RUs when you don't need the response body.
  6. Provisioned throughput sized for peak. Use autoscale (10xโ€“100x flex) or serverless for spiky workloads. Manual scale burns money during quiet hours.

Practical Takeaways

  • Pick the partition key as if you can't change it โ€” because you effectively can't.
  • Default to Session consistency. Promote only when correctness demands it.
  • Point reads (1 RU) are 100x cheaper than cross-partition queries โ€” denormalize aggressively.
  • Use bulk mode for ingest, change feed for projections, and DiskANN for billion-scale vectors.
  • Authenticate with DefaultAzureCredential, not account keys.
  • Disable EnableContentResponseOnWritewhen you don't need the response body.
  • Use serverless or autoscale unless your traffic is genuinely flat.