Azure · lesson 19 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: Change Feed
Read a Cosmos DB container's change feed from the beginning with the Python SDK against the vNext emulator, save its continuation token, make another write, and resume from the token to see only that one new change.
By SkillByExample · Published
A Cosmos DB container’s change feed is a persistent, ordered log of every item that’s been created or updated in it — not a notification that something changed, but the changes themselves, replayable from the beginning or resumed from a saved position. This lesson runs against the same vNext emulator image as the rest of this Cosmos DB sequence, in a fresh container.
Reading from the Beginning, Then Resuming from a Saved Token
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("cfdb")
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)
pager = container.query_items_change_feed(start_time="Beginning").by_page()
seen = [item["id"] for page in pager for item in page]
token = pager.continuation_token
print("change feed from the beginning:", seen)
# a change that happens *after* the token above was captured
container.upsert_item({"id": "o1", "customer": "bob", "total": 20})
resumed_pager = container.query_items_change_feed(continuation=token).by_page()
resumed = [item["id"] for page in resumed_pager for item in page]
print("change feed resumed from the saved token:", resumed)
$ docker run -d --name az-cosmos-feed-demo -e PROTOCOL=http mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-EN20261019
aeb3acc59303d21b65a2fec825ce95dff5f7d2894d2bb861c1ac6604886f2d86
$ until docker logs az-cosmos-feed-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-feed-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_changefeed.py"
[notice] To update, run: pip install --upgrade pip
change feed from the beginning: ['o0', 'o1', 'o2']
change feed resumed from the saved token: ['o1']
Reading from the beginning shows all three orders, each once, in the order they were written —
alice’s two orders (o0, o2) both landed in the same logical partition, so their relative
order is reliable; a real multi-partition container with far more data wouldn’t guarantee that
same ordering across partitions. The resumed read sees only o1 — the one write that happened
after token was captured — not the three orders again. That’s the entire point of saving the
token: a reader that crashes after processing the first feed read can restart from token
instead of re-processing everything from the beginning.
Practice
1. Update an item twice between two change feed reads and confirm it appears only once, with its latest value.
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("cfdb").get_container_client("orders")
pager = container.query_items_change_feed(start_time="Now").by_page()
token = pager.continuation_token
container.upsert_item({"id": "o2", "customer": "alice", "total": 60})
container.upsert_item({"id": "o2", "customer": "alice", "total": 70})
resumed = container.query_items_change_feed(continuation=token).by_page()
changes = [(item["id"], item["total"]) for page in resumed for item in page]
print("changes since token:", changes)
$ docker run --rm --network container:az-cosmos-feed-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_changefeed_latest_only.py"
[notice] To update, run: pip install --upgrade pip
changes since token: [('o2', 70)]
Two writes to the same o2, one feed entry — the change feed in its default mode is a log of
items that changed, not a log of every individual write, so the entry shown carries whatever
o2’s value was by the time the feed read caught up to it (70, not 60). This task reads
continuation_token right off the pager without iterating it first, unlike the main example
and Practice 3 above, which both drain every page before reading the token. That’s safe here
specifically because start_time="Now" has nothing before it to drain — the boundary is just
“this instant,” with zero pending pages. start_time="Beginning" can have any number of pages
of history in front of it, so a token read without draining them first would mark the boundary
partway through that backlog instead of at the end of it.
2. Predict what passing partition_key="bob" to the change feed read does, then run it and check — it doesn't do what the SDK's own docstring says.
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("cfdb").get_container_client("orders")
pager = container.query_items_change_feed(
start_time="Beginning", partition_key="bob"
).by_page()
ids_and_customers = [
(item["id"], item["customer"]) for page in pager for item in page
]
print("with partition_key='bob':", ids_and_customers)
$ docker run --rm --network container:az-cosmos-feed-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_changefeed_single_partition.py"
[notice] To update, run: pip install --upgrade pip
with partition_key='bob': [('o0', 'alice'), ('o1', 'bob'), ('o2', 'alice')]
All three orders came back, alice’s included, even though partition_key="bob" was passed.
The SDK’s own docstring for query_items_change_feed describes partition_key as scoping the
read to one logical partition, the same way it does for read_item and a single-partition
query — but on this emulator it’s accepted without error and silently has no filtering effect.
That gap between documented and actual behavior is exactly why this lesson runs everything
against a real emulator instead of describing the SDK from its docs: code relying on
partition-scoped change feed reads should verify that assumption against whichever Cosmos DB
it’s actually deployed against, emulator or production, rather than trust the parameter name.
3. Predict whether deleting an item shows up in the default-mode change feed, then run it and check.
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("cfdb").get_container_client("orders")
# read everything that exists so far, to get a clean, unambiguous boundary token
caught_up = container.query_items_change_feed(start_time="Beginning").by_page()
for page in caught_up:
list(page)
token = caught_up.continuation_token
container.delete_item(item="o0", partition_key="alice")
resumed = container.query_items_change_feed(continuation=token).by_page()
seen = [item["id"] for page in resumed for item in page]
print("change feed after a delete (default mode):", seen)
$ docker run --rm --network container:az-cosmos-feed-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_changefeed_delete.py"
[notice] To update, run: pip install --upgrade pip
change feed after a delete (default mode): []
Nothing shows up — a delete is invisible to the change feed in its default mode, which tracks creates and updates only. Code that needs to react to deletes needs the separate all-versions-and-deletes change feed mode, not the one this lesson’s other examples use.
That’s the data-plane core of Cosmos DB this track covers on a local emulator. Consistency levels and global distribution are skipped because the emulator is single-node, with no multi-region behavior to observe; server-side stored procedures, triggers, and UDFs are skipped for an unrelated reason — the vNext emulator doesn’t support them at all, regardless of node count.
That’s the last lesson this track writes directly. AKS, Service Bus’s at-least-once delivery,
and Key Vault’s key operations continue via lessons borrowed from other tracks — see “Also part
of Azure” on the Kubernetes, event-driven, and cryptography pages this track links to. From
here: terraform covers provisioning Azure resources (alongside AWS and GCP) as code with HCL
instead of the CLI, and cloud-interviews covers scenario questions that span all three major
clouds.
Cleanup
$ docker rm -f az-cosmos-feed-demo
az-cosmos-feed-demo