Table of Contents

Introduction

I encourage you to read this post first to get the most out of the information provided here:

I hope that after reading the previous article, conflict resolution in Azure Cosmos DB already feels much more approachable. But things get a bit more interesting when your architecture includes more than one write region. Handling conflicts in multi-write regions in Azure Cosmos DB is where the real fun begins.

In this post, we will raise the complexity bar a little and explore that topic step by step.

By the end, you’ll have a clear understanding of how to handle these scenarios with confidence.

Multi-Write Regions

Multi-write regions allow Azure Cosmos DB to accept writes in multiple locations, giving globally distributed applications faster, more resilient write paths. They’re especially valuable when you want to keep your system responsive during regional disruptions or when strict read consistency isn’t the highest priority and you’re comfortable with that trade‑off.

Choosing the route of multi-write regions has to be preceded by a thorough analysis which is a topic for another post but in a nutshell you might choose multi‑region writes if you need:

  • Lower write latency for users spread across different geographic regions
  • Near‑zero downtime during partial or full regional outages
  • Higher write throughput and resilience, thanks to independent local commits in each region
Architecture diagram which features Multi-write region Azure Cosmos DB deployment with 2 Azure Function Apps writing data to a corresponding region.

Let’s assume you’ve found good reasons to choose this approach and currently have Azure Cosmos DB deployed in only one region which is East US (a.k.a. Region A). This detail is important, so let me repeat it once more (hopefully without sounding like a secondary‑school teacher) – this is your only region right now, so if you add any new ones, this one will still be considered the first.

Okay, the decision has been made, so let’s enable it! But… wait a second. I don’t see the option anywhere.

❗❗❗ In most cases, the reason is simple: your account is running in Serverless mode, and multi‑region writes aren’t supported there. To enable this feature, you need a Provisioned Throughput account instead.

Let’s assume you’ve already addressed that challenge and created a second deployment in the West US region a.k.a. Region B (which, by the way, is the paired region of East US). Great, good job!

Now we need to understand what might be the most important concept in this whole topic: the regions are not equal. One of them plays a more significant role. And if you suspect it’s the first region that was initially added, then you’re absolutely right 🙂 The first region is called the hub region, while all additional regions are considered satellite regions.

Let’s make this small, but very important adjustment to our diagram.

Architecture diagram which features Multi-write region Azure Cosmos DB deployment including the Hub and the Satellite regions with 2 Azure Function Apps writing data to a corresponding region.

As you can see, we now have the East US deployment marked as the hub and the West US deployment marked as the satellite. To complete the picture, let’s add short definitions of each.

Hub region

The primary region in a multi‑write Cosmos DB account. In a multi‑region‑write setup with two or more regions, the first region where your account is created is designated as the hub region.

ℹ️ If you ever remove the hub region, the next region based on the order in which they were added is automatically promoted to become the new hub.

Satellite region

Any additional region added after the hub. Satellites accept local writes, but their changes are later validated and confirmed by the hub region (we will get back to it in a second).

Write lifecycle in multi‑write regions

Now that we understand the roles of the hub and satellite regions, we can move on to the next section: the write lifecycle in multi‑write regions.

Let’s start with the more complicated scenario first, which occurs only in the satellite region.

Satellite (unconfirmed, confirmed)

When a write originates in a satellite region, it first enters the unconfirmed (or tentative) state. The write is committed locally in that satellite region, but it isn’t yet considered final. It must still be sent to the hub region.

Conflict resolution flow in Cosmos DB across hub and satellite regions, showing confirmed and rejected states.

Let’s analyze this diagram to fully understand the flow. As you can see the write is initiated in the Satellite region.

  1. A write operation is initiated in Region B (West US – Satellite) by an Azure Function App.
  2. The write is stored locally (meaning in the West US region) in Azure Cosmos DB – B and enters the UNCONFIRMED state.
  3. The unconfirmed write is sent asynchronously to Region A (East US – Hub), where the configured conflict resolution policy (we will get back to it in a second) evaluates it.
  4. Depending on the conflict resolution policy result:
    • (A)If the hub confirms the write, the state becomes CONFIRMED, and the final version is persisted in Azure Cosmos DB – A and then eventually replicated to other regions.
    • (B)If the hub rejects the write, the satellite’s unconfirmed version is rejected/discarded meaning that the hub’s authoritative version – either its existing confirmed version or a new version produced by a custom conflict resolution is replicated back to the satellite region.

Hub (confirmed)

The situation in the hub region is less complicated. It’s almost strange to even talk about a “write lifecycle” there, because there isn’t one at all.

Diagram showing a write confirmed in the hub region and later replicated to the satellite region in Azure Cosmos DB.
  1. A write operation is initiated in Region A (East US – Hub) by an Azure Function App.
  2. A write issued in the hub region becomes a confirmed write immediately, without entering any unconfirmed/tentative state.
  3. The hub’s confirmed version is then replicated asynchronously to the satellite region B.

How the hub region resolves conflicts

Now that we understand the theory behind the unconfirmed and confirmed states, we can move on to the next topic: how the hub region applies the conflict resolution policy.

Let’s first define what the conflict resolution policy is:

A conflict resolution policy is the mechanism Azure Cosmos DB uses when an unconfirmed write is applied in a secondary region and the hub must decide whether to confirm or reject that operation.

Okay, we know the definition, so let’s delve into the two modes in which this mechanism may run:

Last Write Wins (LWW)

Last Write Wins (LWW) is a conflict resolution policy that selects the winning write based on the highest value in a chosen field, treating that write as the authoritative version and discarding the others.

  • Default behavior: Cosmos DB uses the built‑in _ts system property (a server‑generated timestamp) as the comparison value.
  • Custom behavior (NoSQL API only): You can configure a custom conflict resolution path that points to your own numeric or timestamp field instead of _ts.

This is how it could look if you were configuring it using C#. As you can see, it uses the LastWriterWins enum value and specifies a ResolutionPath (again, only for the NoSQL API). If you leave it empty, Cosmos DB will fall back to the default field path, which is _ts.

var spaceEntitiesContainer = await createClient.GetDatabase(options.DatabaseName)
    .CreateContainerIfNotExistsAsync(new ContainerProperties(options.Collections.SpaceEntities.Name, "/Type")
    {
        ConflictResolutionPolicy = new ConflictResolutionPolicy()
        {
            Mode = ConflictResolutionMode.LastWriterWins,
            ResolutionPath = "/customPropertyOtherThanTheBuiltInTimestamp"
        }
    });

There is one more very important thing to remember:

ℹ️ A conflict resolution policy can only be defined when the container is created, and it cannot be changed afterward.

Custom

A custom conflict resolution policy lets you define your own logic for deciding whether a write from a secondary region should be confirmed or rejected. It requires a merge stored procedure (written by you in JavaScript) that Cosmos DB runs exactly once when a conflict occurs. If the procedure is missing or fails, the conflicting items are written to the conflicts feed for manual resolution.

I think we can end this definition here because this is an option chosen much less often.

Types of conflicts

Now that we’ve covered both conflict resolution modes, we can move on to the next part of the story: the types of conflicts the hub region may encounter when applying these policies.

  • Insert conflict: Occurs when two regions attempt to create a new document with the same id at the same time.
  • Replace conflict: Happens when multiple regions update the same existing document concurrently.
  • Delete conflict: Triggered when one region deletes an item while another region tries to insert or update it

❗❗❗ When a delete conflict occurs, the deleted version always wins, whether the competing operation is an insert or a replace, and regardless of the conflict resolution path value.

Updating CosmosClient in your app

With the conflict resolution mechanics out of the way, we can now shift our attention to something just as important: how your application should update its CosmosClient when you enable multi‑write regions.

Before adding a second region, your CosmosClient might have looked like this (and I hope that after reading this post, you’re no longer using a connection string):

using Azure.Identity;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace DeployedInAzure.MultiWriteRegions
{
    public static class ServiceCollectionsExtensions
    {
        extension(IServiceCollection services)
        {
            public IServiceCollection RegisterCosmosClient(IConfiguration configuration)
            {
                var credential = new DefaultAzureCredential();

                services.AddSingleton(_ => new CosmosClient(configuration["CosmosDb:AccountEndpoint"], credential));

                return services;
            }
        }
    }
}

When you add a second region, whether it’s a write region (the focus of this post) or a read region, you must tell the client which location it’s running in by setting the ApplicationRegion property.

using Azure.Identity;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace DeployedInAzure.MultiWriteRegions
{
    public static class ServiceCollectionsExtensions
    {
        extension(IServiceCollection services)
        {
            public IServiceCollection RegisterCosmosClient(IConfiguration configuration)
            {
                var credential = new DefaultAzureCredential();

                var options = new CosmosClientOptions
                {
                    ApplicationRegion = configuration["CosmosDb:Region"]
                };

                services.AddSingleton(_ => new CosmosClient(configuration["CosmosDb:AccountEndpoint"], credential, options));

                return services;
            }
        }
    }
}

If an account is configured with multiple regions such as East US and West US, configuring the client as shown above will cause the CosmosClient to generate a preferred regions list based on proximity to the ApplicationRegion. The CosmosClient will send requests to the ApplicationRegion, and if that region becomes unavailable, it will automatically fall back to the next region on the proximity list.

ℹ️ You can check this class to see all available region values.

Summary

Multi‑write regions introduce a few new moving parts, but the core ideas are surprisingly straightforward once you see how they fit together.

  • The hub region always has the final say.
  • Satellite regions submit unconfirmed writes for validation.
  • The conflict resolution policy decides which version wins when two regions touch the same item.

We have also learned that adding a proper client configuration with ApplicationRegion allows your app to automatically route writes (and reads too) to the closest region and fail over smoothly when needed.

I hope that what I’ve walked you through here makes the whole idea of multi‑write region concurrency feel a bit more approachable. It’s a complex topic, but once you see how the pieces fit together, it starts to make a lot more sense.

Thanks a lot for reading this post and see you in the next one!

Categorized in:

Databases,