RO EN

Cosmos DB patterns (3) — multi-region writes

Cosmos DB patterns (3) — multi-region writes ✨ Imagine generată cu AI
Doru Bulubașa
29 July 2026
40 views

The third part of the series about advanced patterns in Cosmos DB. After partition keys and Change Feed, we move to the global level: writes in multiple regions.


Single-region writes with read replicas — the starting point

The first important nuance: you don't need multi-region writes to serve global reads. The classic configuration — one write region + read replicas in other regions — covers most scenarios: users read from the nearby region, all writes go to the main region.

# Add a read region
az cosmosdb update \
  --name my-cosmos --resource-group my-rg \
  --locations regionName=westeurope failoverPriority=0 isZoneRedundant=true \
  --locations regionName=eastus failoverPriority=1 isZoneRedundant=true

Multi-region writes come into discussion only when:

  • Write latency matters globally — active users on multiple continents who write frequently (a chat, a collaborative editor), and the round-trip to the single write region is unacceptable
  • Write availability everywhere — you want a failed region not to stop writes anywhere (near-zero RTO, automatic failover)

The price: conflict complexity. If your audience is regionally concentrated (e.g., customers from Romania and the EU), a single write region in West Europe is simpler and sufficient — a conclusion that applies directly to a regional SaaS.


Enabling multi-region writes

az cosmosdb update \
  --name my-cosmos --resource-group my-rg \
  --enable-multiple-write-locations true
// .NET SDK: tell the client which regions it prefers, in order
builder.Services.AddSingleton(_ => new CosmosClient(
    accountEndpoint: endpoint,
    tokenCredential: new DefaultAzureCredential(),
    clientOptions: new CosmosClientOptions
    {
        // The instance from Europe writes/reads in West Europe,
        // the one from US in East US -- each local
        ApplicationPreferredRegions = new List<string>
        {
            "West Europe",
            "East US"
        }
    }));

With ApplicationPreferredRegions, each app instance (deployed regionally) writes in its own region — local write latency. The SDK automatically fails over through the preference list if the first region fails.


Consistency levels in a multi-region context

With writes in multiple places, the consistency level becomes a visible decision:

Level Guarantee Multi-region note
Strong Global linearizability Incompatible with multi-region writes on accounts with distant regions — latency would be global RTT
Bounded Staleness Maximum delay of K versions / T seconds Good compromise when you want predictability
Session Consistency within own session The default and the correct choice for most applications — you read your own writes
Consistent Prefix Write order preserved Rare explicit choice
Eventual Convergence, no order Only for data where order doesn't matter

Session consistency is worth emphasizing: a user who writes a message sees it immediately in their own session, even if global replication takes time. For a chat or a per-client dashboard, that's exactly what you want.


Conflicts: inevitable, so managed

Two regions write the same document almost simultaneously. Replication brings them face to face: conflict. Cosmos DB offers two resolution policies, set per container, at creation:

Last-Writer-Wins (default)

The document with the higher value on a numeric property wins (default _ts, the timestamp). Simple and sufficient when losing a concurrent write is acceptable:

var containerProperties = new ContainerProperties("sessions", "/tenantId")
{
    ConflictResolutionPolicy = new ConflictResolutionPolicy
    {
        Mode = ConflictResolutionMode.LastWriterWins,
        // You can use your own property instead of _ts
        ResolutionPath = "/updatedEpoch"
    }
};
await database.CreateContainerIfNotExistsAsync(containerProperties);

Custom with conflict feed

When silent loss is not acceptable (e.g., two concurrent updates on a tenant's configuration that must be combined, not overwritten), choose Custom: unresolved conflicts go into a conflict feed that you process:

// Custom policy without stored procedure -- conflicts go to feed
var props = new ContainerProperties("tenant-config", "/tenantId")
{
    ConflictResolutionPolicy = new ConflictResolutionPolicy
    {
        Mode = ConflictResolutionMode.Custom
    }
};

// Conflict processing -- periodic job or BackgroundService
var conflicts = _container.Conflicts;
using var iterator = conflicts.GetConflictQueryIterator<ConflictProperties>();
while (iterator.HasMoreResults)
{
    foreach (var conflict in await iterator.ReadNextAsync())
    {
        var current = conflicts.ReadCurrentConflictContent<TenantConfig>(conflict);
        // Your merge logic: combine, choose, or raise alert
        var merged = MergeConfigs(current /*, ... */);
        await _container.UpsertItemAsync(merged, new PartitionKey(merged.TenantId));
        await conflicts.DeleteAsync(conflict, new PartitionKey(merged.TenantId));
    }
}

A design rule more valuable than any policy: model your data to avoid conflicts. Per-user or per-session documents (not shared), append-only operations (new messages, no updates on the same document) make conflicts almost impossible by design.


What’s next

Conflicts between regions have a smaller and much more frequent sibling: concurrent writes on the same document in the same region. In the fourth part: optimistic concurrency with ETags — the pattern that prevents lost updates.

Questions? Write me at contact@ludoprogramming.com.