Introduction
When you start working with any database, there are a few things you usually need to figure out first to feel confident that you can handle the challenges that may arise in your project.
One of these challenges (some would even say the most important) is how to deal with issues that stem from concurrency.
In Azure Cosmos DB, this challenge can be even more significant because of the nature of the database, which is distributed by design.
In this post, I will briefly explain what the ETag field is and how you can use it to make your applications more reliable.
Before we delve into the technical details, let’s first make sure we understand the concept of concurrency.
Concurrency
Before diving into specific mechanisms, it’s important to understand what concurrency means in the context of databases. Concurrency occurs when multiple processes or users attempt to modify the same data at the same time. Without proper control, this can lead to conflicts, overwrites, or inconsistent results.
We can divide concurrency into two categories: Optimistic Concurrency and Pessimistic Concurrency. We will focus on the first category but I think it’s worth to know both conceps.
Optimistic Concurrency
This approach assumes that conflicts are rare. Multiple operations can proceed without locking data, and the system checks for conflicts only when changes are committed. If a conflict is detected, the operation fails and can be retried.
Azure Cosmos DB has built‑in support for optimistic concurrency.
Pesimistic Concurrency
This approach assumes that conflicts are likely. It prevents them by locking data before an operation begins, ensuring that no other process can modify the resource until the lock is released. This guarantees consistency but can reduce performance due to blocking.
Azure Cosmos DB does not provide built‑in support for pessimistic locking.
ℹ️ It is possible to achieve pessimistic locking in conjunction with Cosmos DB using a distributed locking mechanism, but doing so may undermine the core advantages of Cosmos DB’s design.
ETag field
An ETag is a system generated identifier that represents the version of a document. Every time a document is updated, Cosmos DB automatically changes its ETag. This makes the ETag a reliable way to detect whether a document has been modified since you last read it.
ℹ️ If you are familiar with EF Core and SQL Server, then you can think about ETag like the Version (rowversion) field in SQL Server.
Let’s assume we are working with the following Azure Cosmos DB document representing a space entity:
{
"id": "1",
"Name": "Sirius",
"Type": "Star",
"Description": "Sirius is the brightest star in the night sky, located in the constellation Canis Major.",
"_rid": "9oRCANrQQLABAAAAAAAAAA==",
"_self": "dbs/9oRCAA==/colls/9oRCANrQQLA=/docs/9oRCANrQQLABAAAAAAAAAA==/",
"_etag": "\"00005800-0000-5600-0000-693d14210000\"",
"_attachments": "attachments/",
"_ts": 1765610529
}As you can see in line 8, there is an _etag system property and below is a C# class that we will use.
namespace DeployedInAzure.ConcurrencyAndETag
{
public record SpaceEntityDocument
{
public required string id { get; init; }
public required string Name { get; init; }
public required string Type { get; init; }
public required string Description { get; init; }
}
}Let’s implement a very simple repository without any concurrency support.
using Microsoft.Azure.Cosmos;
namespace DeployedInAzure.ConcurrencyAndETag
{
public class SpaceEntitiesRepository(Container container)
{
public async Task<SpaceEntityDocument?> GetAsync(string id, string partitionKey)
{
try
{
return await container.ReadItemAsync<SpaceEntityDocument>(id, new PartitionKey(partitionKey));
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
}
public async Task UpsertAsync(SpaceEntityDocument document, string partitionKey)
{
await container.UpsertItemAsync(document, new PartitionKey(partitionKey));
}
}
}
IfMatch
The If-Match condition is the key to implementing optimistic concurrency in Azure Cosmos DB. It ensures that an update or replace operation only succeeds if the document’s current ETag matches the one you provide.
In other words, you are telling Cosmos DB:
Update this document only if it hasn’t changed since I last read it.
If the ETag has changed (meaning another process updated the document in the meantime), the operation fails with a 412 Precondition Failed status code.
Let’s update our repository then!
using Microsoft.Azure.Cosmos;
using System.Net;
namespace DeployedInAzure.ConcurrencyAndETag
{
public class SpaceEntitiesRepositoryV2(Container container)
{
public async Task<(SpaceEntityDocument? Document, string? ETag)> GetAsync(
string id,
string partitionKey,
CancellationToken cancellationToken)
{
try
{
var response = await container.ReadItemAsync<SpaceEntityDocument>(
id,
new PartitionKey(partitionKey),
cancellationToken: cancellationToken);
return (response.Resource, response.ETag);
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return (null, null);
}
}
public async Task UpsertAsync(
SpaceEntityDocument document,
string partitionKey,
string etag,
CancellationToken cancellationToken)
{
try
{
var options = new ItemRequestOptions
{
IfMatchEtag = etag
};
await container.UpsertItemAsync(
document,
new PartitionKey(partitionKey),
options,
cancellationToken);
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed) // -> 412
{
// Handle concurrency conflict by throwing an exception
// or returning a specific result depending on your application's needs.
}
}
}
}
In this updated repository, GetAsync now returns both the document and its ETag, while UpsertAsync uses the IfMatchEtag option to ensure updates only succeed if the document has not changed since it was last read. If the ETag does not match, Cosmos DB returns a 412 (Precondition Failed), signaling a concurrency conflict.
We can agree this mechanism is very convenient for developers. Things stay simple with a single write region, but multi‑write regions add a bit more complexity. I will dedicate a separate post to make sure that aspect is clear and approachable so stay tuned!
IfNoneMatch (for reads)
Let me raise one more issue. To fully use the potential of the _etag field, consider this: since the ETag tells us the version of a document, if we already have that version locally and want to check whether Cosmos DB has something newer, we can simply ask:
Hey Cosmos DB, if you have something newer than what I have locally, please return it to me. Otherwise, just let me know that I already have the latest version.
This is exactly what the If-None-Match condition does. Instead of always re-downloading a document, you provide the ETag you currently hold. Cosmos DB compares it against the latest version and either returns the updated document or responds with 304 Not Modified if nothing has changed.
Let’s enhace our repository code one more time!
public async Task<(SpaceEntityDocument? Document, string? ETag)> GetAsync(
string id,
string partitionKey,
string? eTag,
CancellationToken cancellationToken)
{
try
{
var options = new ItemRequestOptions();
if (!string.IsNullOrEmpty(eTag))
{
options.IfNoneMatchEtag = eTag;
}
var response = await container.ReadItemAsync<SpaceEntityDocument>(
id,
new PartitionKey(partitionKey),
options,
cancellationToken);
return (response.Resource, response.ETag);
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotModified) // -> 304
{
return (null, eTag);
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return (null, null);
}
}The heart of that code lies in the IfNoneMatchEtag property of the ItemRequestOptions object.
By setting options.IfNoneMatchEtag = eTag, you’re telling Cosmos DB “Only return the document if its current ETag is different from the one I already have.“
- If the ETag matches (meaning nothing has changed), Cosmos DB responds with 304 Not Modified.
- If the ETag is different, Cosmos DB sends back the updated document along with its new ETag.
This makes reads more efficient because you avoid re‑downloading unchanged data.
Summary
I hope you’ve already spotted a few places in your application that could benefit from leveraging the ETag field. Whether it’s preventing accidental overwrites with IfMatchEtag or saving bandwidth with IfNoneMatchEtag, these simple mechanisms can make your Cosmos DB interactions safer and more efficient. And since the concepts mirror familiar patterns like SQL Server’s rowversion, adopting them should feel natural.
Thanks a lot for reading and see you again!