Azure · lesson 13 of 19
Beginner
4Intermediate
13- 5 Azure Virtual Network Basics
- 6 Azure SQL Basics
- 7 Azure Functions Fundamentals
- 8 Bicep and ARM Templates Basics
- 9 Azure Monitor Basics
- 11 Azure Cosmos DB: Items and Partition Keys
- 12 Azure Cosmos DB: Queries
- 13 Azure Cosmos DB: ETags and Patch
- 14 Azure Service Bus: Queues and Settlement
- 15 Azure Service Bus: Dead-Lettering
- 16 Azure Service Bus: Topics and Filters
- 17 Azure Key Vault: Secrets and Versions
- 18 Azure Key Vault: Soft-Delete and Recovery
Also part of Azure
5- + Dead-Letter Queues: What Happens When Retries Keep Failing from Event-Driven Architecture
- + Idempotent Consumers: Surviving a Duplicate Delivery from Event-Driven Architecture
- + Envelope Encryption and Key Rotation Without Re-encrypting Data from Cryptography
- + Deployments and Rollouts from Kubernetes
- + Services and Networking from Kubernetes
Azure Cosmos DB: ETags and Patch
Patch a Cosmos DB item atomically with the Python SDK, then see what an optimistic-concurrency replace actually does: a stale ETag raises a 412, and the fix is reading the item's current ETag again before retrying, not just resubmitting the same write.
By SkillByExample · Published
Two different tools solve two different concurrency problems in Cosmos DB. patch_item applies
a small operation directly on the server, atomically, with no read step to race against.
replace_item with an ETag makes an ordinary read-modify-write safe by making the write fail
loudly instead of silently overwriting someone else’s change. This lesson runs against the
same vNext emulator image as the previous two, in its own fresh container.
patch_item Increments Without a Read-Modify-Write Race
from azure.cosmos import CosmosClient, PartitionKey
ENDPOINT = "http://localhost:8081"
KEY = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
client = CosmosClient(ENDPOINT, credential=KEY)
db = client.create_database_if_not_exists("storedb")
container = db.create_container_if_not_exists(
id="orders", partition_key=PartitionKey(path="/customer")
)
for order in [
{"id": "o0", "customer": "alice", "total": 30},
{"id": "o1", "customer": "bob", "total": 12},
{"id": "o2", "customer": "alice", "total": 55},
]:
container.upsert_item(order)
before = container.read_item(item="o1", partition_key="bob")
print("o1 total before patch:", before["total"])
patched = container.patch_item(
item="o1",
partition_key="bob",
patch_operations=[{"op": "incr", "path": "/total", "value": 5}],
)
print("o1 total after incr patch:", patched["total"])
$ docker run -d --name az-cosmos-patch-demo -e PROTOCOL=http mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-EN20261019
a8af5c9bf15764f1a2e35bc5e2057509da322f0e63b2872c247e2ab4aeac4725
$ until docker logs az-cosmos-patch-demo 2>&1 | grep -q 'System is now fully ready to accept requests'; do sleep 0.5; done
$ docker run --rm --network container:az-cosmos-patch-demo -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-cosmos==4.17.1 2>&1 | tail -1 && python cosmos_patch.py"
[notice] To update, run: pip install --upgrade pip
o1 total before patch: 12
o1 total after incr patch: 17
incr adds its value to whatever /total currently is, server-side — there’s no window where
the client has read 12 into memory and another writer could sneak a change in before this
patch applies. A read-modify-upsert_item cycle doing total = read().total + 5 has exactly
that window; patch_item with incr doesn’t.
A Stale ETag Makes replace_item Fail Loudly Instead of Overwriting Silently
from azure.cosmos import CosmosClient
from azure.cosmos.exceptions import CosmosAccessConditionFailedError
from azure.core import MatchConditions
ENDPOINT = "http://localhost:8081"
KEY = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
client = CosmosClient(ENDPOINT, credential=KEY)
container = client.get_database_client("storedb").get_container_client("orders")
stale = container.read_item(item="o1", partition_key="bob")
stale_etag = stale["_etag"]
# another process updates the item in between — this write has nothing to do with the
# etag we captured above, so the item's current _etag is now different from `stale_etag`
container.upsert_item({"id": "o1", "customer": "bob", "total": 999})
try:
container.replace_item(
item="o1",
body={"id": "o1", "customer": "bob", "total": 1000},
etag=stale_etag,
match_condition=MatchConditions.IfNotModified,
)
except CosmosAccessConditionFailedError as e:
print("replace with stale etag ->", type(e).__name__, e.status_code)
fresh = container.read_item(item="o1", partition_key="bob")
retried = container.replace_item(
item="o1",
body={"id": "o1", "customer": "bob", "total": 1000},
etag=fresh["_etag"],
match_condition=MatchConditions.IfNotModified,
)
print("retry with current etag -> total:", retried["total"])
$ docker run --rm --network container:az-cosmos-patch-demo -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-cosmos==4.17.1 2>&1 | tail -1 && python cosmos_etag_conflict.py"
[notice] To update, run: pip install --upgrade pip
replace with stale etag -> CosmosAccessConditionFailedError 412
retry with current etag -> total: 1000
The first replace_item fails because stale_etag was captured before the concurrent
upsert_item ran — exactly the race optimistic concurrency is meant to catch. The fix isn’t to
retry with the same stale_etag (that fails again forever) or to drop the etag condition
entirely (that overwrites 999 without ever looking at it) — it’s reading the item again to get
its current _etag, deciding the write is still correct, and retrying with that value.
Practice
1. Use the add and set patch operations to create and then overwrite a field in one patch_item call, then remove it with a second call.
from azure.cosmos import CosmosClient
ENDPOINT = "http://localhost:8081"
KEY = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
client = CosmosClient(ENDPOINT, credential=KEY)
container = client.get_database_client("storedb").get_container_client("orders")
container.patch_item(
item="o0",
partition_key="alice",
patch_operations=[
{"op": "add", "path": "/status", "value": "pending"},
{"op": "set", "path": "/status", "value": "shipped"},
],
)
shipped = container.read_item(item="o0", partition_key="alice")
print("status after add+set:", shipped["status"])
container.patch_item(
item="o0",
partition_key="alice",
patch_operations=[{"op": "remove", "path": "/status"}],
)
after_remove = container.read_item(item="o0", partition_key="alice")
print("has status after remove:", "status" in after_remove)
$ docker run --rm --network container:az-cosmos-patch-demo -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-cosmos==4.17.1 2>&1 | tail -1 && python cosmos_patch_set_remove.py"
[notice] To update, run: pip install --upgrade pip
status after add+set: shipped
has status after remove: False
add on a path that doesn’t exist yet creates it; set (used here on a path that already
exists) replaces its value; remove deletes the field entirely rather than setting it to null.
2. Call patch_item with a condition expression that's false and predict what that raises.
from azure.cosmos import CosmosClient
from azure.core.exceptions import HttpResponseError
ENDPOINT = "http://localhost:8081"
KEY = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
client = CosmosClient(ENDPOINT, credential=KEY)
container = client.get_database_client("storedb").get_container_client("orders")
before = container.read_item(item="o2", partition_key="alice")
print("o2 total before:", before["total"])
try:
container.patch_item(
item="o2",
partition_key="alice",
patch_operations=[{"op": "incr", "path": "/total", "value": 1000}],
filter_predicate="FROM c WHERE c.total > 10000",
)
except HttpResponseError as e:
print("conditional patch with false predicate ->", type(e).__name__, e.status_code)
after = container.read_item(item="o2", partition_key="alice")
print("o2 total after (unchanged):", after["total"])
$ docker run --rm --network container:az-cosmos-patch-demo -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-cosmos==4.17.1 2>&1 | tail -1 && python cosmos_patch_condition.py"
[notice] To update, run: pip install --upgrade pip
o2 total before: 55
conditional patch with false predicate -> CosmosAccessConditionFailedError 412
o2 total after (unchanged): 55
A filter_predicate turns the whole patch into a no-op-or-fail check: when the condition doesn’t
match the item’s current state, Cosmos DB rejects the entire patch (a 412, the same status a
failed ETag condition raises) rather than silently skipping just the operations that depend on it.
3. Call replace_item with no etag or match_condition at all, and confirm it overwrites unconditionally even though another write happened in between.
from azure.cosmos import CosmosClient
ENDPOINT = "http://localhost:8081"
KEY = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
client = CosmosClient(ENDPOINT, credential=KEY)
container = client.get_database_client("storedb").get_container_client("orders")
# another process updates o1 first, same as the etag example above
container.upsert_item({"id": "o1", "customer": "bob", "total": 2000})
# this replace carries no etag and no match_condition at all
unconditional = container.replace_item(
item="o1", body={"id": "o1", "customer": "bob", "total": 3000}
)
print("unconditional replace -> total:", unconditional["total"])
$ docker run --rm --network container:az-cosmos-patch-demo -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-cosmos==4.17.1 2>&1 | tail -1 && python cosmos_replace_no_etag.py"
[notice] To update, run: pip install --upgrade pip
unconditional replace -> total: 3000
No 412, no error of any kind — replace_item without an etag/match_condition pair is
unconditional by default, the same way a plain upsert_item is. The optimistic-concurrency
protection earlier in this lesson exists only because the code explicitly asked for it; a write
that doesn’t pass an etag always wins the race, overwriting whatever was there regardless of
whether it already changed since the last read.
Cleanup
$ docker rm -f az-cosmos-patch-demo
az-cosmos-patch-demo
Next: Azure Service Bus — sending and receiving messages through a queue, and what it means to settle one.