Keep Downstream Views in Sync with Cosmos DB Change Feed
Use the Azure Cosmos DB SQL API change feed to stream inserts, updates, and deletes to caches and read models without polling the whole container.
13 Jul 2026, 18:31 UTC

Your checkout API updates an order in Azure Cosmos DB, but the search index, inventory cache, and notification service still show the old state. Polling the container wastes request units and lags behind writes. The useful takeaway: the Azure Cosmos DB SQL API change feed is a durable, ordered-per-partition stream of item mutations, so downstream systems can react to inserts, updates, replaces, and deletes without scanning the entire container.
It is not a general-purpose event bus. Ordering is guaranteed only within a single partition key, and deleted items usually carry only their ID and partition key. Treat it as a change stream for materialized views, cache invalidation, and downstream synchronization—not as an immutable audit log.
What the change feed streams
Every SQL API container has a change feed enabled by default. It emits item inserts, updates, replaces, and deletes in the order they occur within each partition key. A partition key is the logical grouping you choose when you create the container; it also determines how Cosmos DB scales storage and throughput. Throughput is billed in request units, or RUs, which represent the cost of reads, writes, and change feed queries.
Checkpoints, leases, and idempotent consumers
A change feed processor uses a separate lease container to store checkpoints and distribute work across consumer instances. A checkpoint records how far a consumer has read in a partition. Leases let a consumer resume after a failure and let you scale out by adding instances. Because the processor can redeliver changes after a lease moves or a transient failure, your handler should be idempotent. Use the item etag, the _ts timestamp, or a business deduplication key to skip events you have already processed.
Worked example: sync a read model
This example uses the .NET SDK pattern for illustration. Class names and lease document formats differ across .NET, Java, Python, and JavaScript SDKs, so verify against the SDK version you actually run before copying code.
Run this on a local development machine, virtual machine, or Azure Function. You need a SQL API container, a separate lease container, and permissions to read the source container and write to the lease container. In Azure RBAC, that is typically the Cosmos DB Built-in Data Contributor role; alternatively, use a valid account key or connection string.
string connectionString = "COSMOSDB_CONNECTION_STRING";
string databaseId = "DATABASE_ID";
string containerId = "CONTAINER_ID";
string leaseContainerId = "LEASE_CONTAINER_ID";
// Create or use an existing lease container before starting the processor.
// The processor writes checkpoint documents to this container.
var processor = changeFeedProcessorBuilder
.WithSourceContainer(sourceContainer)
.WithLeaseContainer(leaseContainer)
.WithHandler((changes, cancellationToken) =>
{
foreach (var change in changes)
{
// Log id, _ts, and operation type for verification.
Console.WriteLine($"{change.Id} | {change._ts} | {change.OperationType}");
// Apply to downstream read model here. Make this step idempotent.
}
return Task.CompletedTask;
})
.Build();
await processor.StartAsync(cancellationToken);
Expected check: after you insert, update, and delete test items in the source container, the consumer log should list each change’s ID, _ts, and operation type in the same order they occurred within each partition key. Risk: change feed reads consume normal request units, so high write churn or many consumers can increase cost and throttle foreground traffic on shared provisioned throughput.
Trade-offs and limits
- Ordering is per partition key only. Workflows that need global order must sequence events downstream or use a single partition key, which limits scalability.
- Autoscale accounts support change feed, but available RU/s varies with load. Size the lease container and consumer concurrency for peak autoscale capacity, not average usage.
- Deleting or recreating the lease container resets checkpoints and can cause full reprocessing. Treat lease state as operational data and use stable container names.
- Change feed does not provide multi-item transactions. Atomicity remains scoped to a partition key and transactional batch; consumers operate after the commit.
Verify before you rely on it
- Create a SQL API container and a separate lease container in the same database account.
- Start the change feed processor with a handler that logs each change’s ID, _ts, and operation type.
- Insert, update, and delete test items across multiple partition keys.
- Confirm the consumer log matches the source mutations per partition key.
- Kill and restart the consumer instance. It should resume from the latest lease checkpoint instead of reprocessing from the beginning.
- Send duplicate changes intentionally or simulate a transient failure, and confirm your handler skips already-processed items using etag, _ts, or a business key.
- Inspect request charge in SDK responses or Azure portal metrics while the feed runs under load. Make sure foreground workload RU/s stays within your provisioned or autoscale limits.
State-change note: creating the lease container is an operational change. If you delete it during a trial, checkpoints are lost and the processor restarts from the beginning. There is no undo for lease state, so do not delete a production lease container expecting to resume where you left off.
Closing
Start with one container and one consumer. Measure RU cost under your actual write churn, make the handler idempotent, and treat the lease container as operational data. If you need global ordering, sequence events downstream or accept the scalability limits of a single partition key.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.