Skip to main content
Azure · lesson 19 of 19
Azure advanced Lesson 19 of 19

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

azure cosmos-db nosql change-feed

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

Frequently Asked Questions

What order does the Cosmos DB change feed return items in?
Each logical partition's changes come back in the order they happened, but the feed is not one single global timeline across partitions — an item in partition A that changed before an item in partition B can still appear after it, because the change feed reads are parallelized per physical partition. Code that needs a true global order has to add its own sequencing (a timestamp field, a monotonic counter) rather than relying on change feed read order across partitions.
Does the change feed show every historical version of an item, or just its latest state?
Just its latest state as of when the feed reader catches up to it. If an item changed three times before a reader ever read the feed, reading from the beginning shows that item once, with its current value — not three separate entries for each intermediate write. A reader that needs every intermediate version needs the change feed's separate all-versions-and-deletes mode, not the default.
What does a saved continuation token actually let a reader resume?
Resuming from exactly where that reader left off: query_items_change_feed(continuation=token) returns only the changes that happened after the point the token was captured, not the whole feed again. That's what makes change feed processing restartable — a worker that crashes or redeploys can pick up from its last saved token instead of reprocessing every change from the beginning.
Can the change feed be read without first saving a continuation token?
Yes, with start_time='Beginning' for every historical change, or the default (now) to only see changes from this point forward. Either way, the SDK still hands back a continuation_token once the read completes, which existing code should save precisely so a later run doesn't have to pick one of those two starting points again.