Introduction

So Cosmos DB is fast. Like really fast. By design CosmosDB operations don't lock your collection or your documents when you are changing them. This means that Cosmos operations can be really fast but it also means that 2 different services or even actions can change a single document at the same time and one of them won't know (by default) that it's update was actually overridden. Cosmos DB uses something called, "Last write wins" which means that the last call to write a change in Cosmos DB is the one that Cosmos DB considers the latest valid version of a document.

Optimistic concurrency in Cosmos DB

Even though the default behaviour is "Last write wins", there are many cases where you want to make sure that you are only changing the latest version of a document. Cosmos DB offers a mechanism to do just that. It provides optimistic concurrency by offering an entity tag or an Etag as part of the document and the response header of a request. This tag can be used with an AccessCondition in order to ensure that if the document changed between the retrieval and the manipulation attempt of the document, we won't update an outdated document but we will get an error instead.

Etag

The entity tag or Etag is a system defined meta-property which is part of every single Resource in Cosmos DB with the property name _etag. It is also returned as a header value in Cosmos DB responses with header key name Etag. It looks like a Guid wrapped in escaped double quotes: "_etag": "\"00000000-0000-0000-9546-e10f04cb01d4\"".

You can provide the Etag with your Update, Upsert or Delete operations in order to enforce optimistic concurrency. The way you do that in code is to provide an object called AccessCondition as part of your RequestOptions. However this is not the only thing you need to provide as part of the AccessCondition object. You also need to specify the behaviour that you want the server to enforce when you provide this Etag.

Here is what the AccessCondition object looks like.

public sealed class AccessCondition  
{
  public AccessConditionType Type { get; set; }
  public string Condition { get; set; }
}

(This is C# but it's exactly the same in any language)

The Condition property is where you need to provide the Etag for this operation.
The Type is an enum of type AccessConditionType. It has two potential values and it looks like this:

public enum AccessConditionType  
{
  IfMatch,
  IfNoneMatch,
}
  • IfMatch will check if the resource's ETag value matches the ETag value provided in the Condition property. This is applicable only on PUT and DELETE which means Update, Upsert and Delete operations.
  • IfNoneMatch will check if the resource's ETag value does not matches the ETag value provided in the Condition property. This is applicable only on GET.

If the AccessCondition is not satisfied during a request then Cosmos will reject the operation and it will return an HTTP 412 Precondition failure response code or an exception with this status code if you are using the SDKs.

Providing access condition samples

HTTP Request

The Cosmos DB documentation implies that there are 3 access conditions headers that you can provide as part of your request.

  • If-Match (String, applicable only on PUT and DELETE) - Used to make operation conditional for optimistic concurrency. The value should be the etag value of the resource.
  • If-None-Match (String, applicable only on GET) - Makes operation conditional to only execute if the resource has changed. The value should be the etag of the resource.
  • If-Modified-Since (Date, applicable only on GET) - Returns etag of resource modified after specified date in RFC 1123 format. Ignored when If-None-Match is specified.

Providing any of these headers as part of your HTTP request to Cosmos will enable the AccessCondition logic.

Cosmos DB v2 .NET SDK

var ac = new AccessCondition { Condition = etag, Type = AccessConditionType.IfMatch };  
await client.ReplaceDocumentAsync(docToReplace, new RequestOptions { AccessCondition = ac });  

Cosmos DB v3 .NET SDK

var ac = new AccessCondition { Condition = etag, Type = AccessConditionType.IfMatch };  
await container.Items.ReplaceItemAsync<Item>(item.id, item.id, item, new CosmosItemRequestOptions { AccessCondition = ac });  

Cosmonaut

var ac = new AccessCondition { Condition = etag, Type = AccessConditionType.IfMatch };  
await cosmosStore.Update(entity, new RequestOptions { AccessCondition = ac });  

NodeJS SDK

await item.replace(person, { accessCondition: { type: "IfMatch", condition: person._etag } });  
//or
await container.items.upsert(person, { accessCondition: { type: "IfMatch", condition: person._etag } });