Enable and Consume Azure Cosmos DB Change Feed for Event‑Driven Processing
Learn how to turn Cosmos DB into an event source by enabling the Change Feed, setting up a lease container, and using the Change Feed Processor Library in C#. The guide covers configuration, validation, monitoring, and fallback strategies for reliable event handling.
01 Jul 2025, 21:43 UTC

Desired Outcome
Build a lightweight, scalable event pipeline that reacts to every insert, update, or delete in a Cosmos DB container without polling the database directly. The goal is to capture changes, process them in batches, and avoid duplicate or missed events while keeping costs predictable.
Prerequisites
- Azure subscription with a Cosmos DB account (SQL API).
- Visual Studio 2022 or later (or any C# IDE). .NET 6+ SDK installed.
- Azure Storage account for the dead‑letter queue (optional but recommended).
- Permissions:
Cosmos DB account reader/writerandStorage account queue contributor. - Basic understanding of partition keys and RU‑based throughput.
Step 1 – Enable Change Feed on the Container
Change Feed is enabled by default when a container is created, but you can explicitly set options via the SDK or portal. The most important setting is the RetentionPeriodInHours – the window during which changes are kept before they expire.
using Microsoft.Azure.Cosmos;
string endpoint = "https://.documents.azure.com:443/";
string key = "";
CosmosClient client = new CosmosClient(endpoint, key);
Database db = await client.CreateDatabaseIfNotExistsAsync("EventDb");
ContainerProperties props = new ContainerProperties("Events", "/partitionKey")
{
ChangeFeedPolicy = new ChangeFeedPolicy
{
RetentionPeriodInHours = 48 // Extend beyond default 24h if processing may lag
}
};
Container container = await db.CreateContainerIfNotExistsAsync(props, throughput: 400);
Verify via the Azure Portal: navigate to the container, click the Change Feed blade, and confirm the retention period.
Step 2 – Create a Lease Container
The Change Feed Processor Library uses a lease container to coordinate work among multiple workers and to keep track of the last processed sequence number. The lease container must share the same partition key as the source container.
ContainerProperties leaseProps = new ContainerProperties("Events-Leases", "/partitionKey")
{
// Default throughput is fine for most scenarios; adjust if you have high change rates
};
await db.CreateContainerIfNotExistsAsync(leaseProps, throughput: 400);
Check that the lease container exists and has the correct partition key. If the container already exists but has a different key, delete it and recreate.
Step 3 – Set Up the Change Feed Processor (C# Example)
The processor handles polling, batching, and lease management automatically. Below is a minimal console app that logs every change.
using Microsoft.Azure.Cosmos.ChangeFeed;
using Microsoft.Azure.Cosmos.ChangeFeed.FeedProcessor;
using Microsoft.Azure.Cosmos.ChangeFeed.FeedProcessor.PartitionCheckpointer;
string endpoint = "https://.documents.azure.com:443/";
string key = "";
CosmosClient client = new CosmosClient(endpoint, key);
string databaseId = "EventDb";
string containerId = "Events";
string leaseContainerId = "Events-Leases";
ChangeFeedProcessorBuilder builder = client.GetChangeFeedProcessorBuilder<EventItem>("processorDemo", ProcessChangesAsync)
.WithInstanceName("processor-instance-1")
.WithLeaseContainer(client.GetContainer(databaseId, leaseContainerId))
.WithFeedContainer(client.GetContainer(databaseId, containerId))
.WithStartTime(DateTime.MinValue); // Start at earliest available change
ChangeFeedProcessor processor = builder.Build();
await processor.StartAsync();
Console.WriteLine("Processor started – press any key to stop.");
Console.ReadKey();
await processor.StopAsync();
async Task ProcessChangesAsync(ChangeFeedProcessorContext context, IReadOnlyCollection<EventItem> changes, CancellationToken cancellationToken)
{
foreach (var item in changes)
{
Console.WriteLine($"[{DateTime.UtcNow}] Received change for id={item.Id}");
// TODO: your business logic here
}
}
public class EventItem
{
public string Id { get; set; }
public string PartitionKey { get; set; }
public string Data { get; set; }
}
Key points:
WithStartTimecontrols where the processor begins.DateTime.MinValuestarts from the oldest retained change.- Leases are stored in the lease container; if a worker crashes, another will pick up the lease after the lease timeout (default 30 s).
- Processing is idempotent if you design your logic to handle duplicates.
Step 4 – Validate Operation
- Insert a test document:
var testDoc = new EventItem { Id = Guid.NewGuid().ToString(), PartitionKey = "demo", Data = "test" }; await client.GetContainer(databaseId, containerId).CreateItemAsync(testDoc, new PartitionKey(testDoc.PartitionKey)); - Observe the console output. The processor should log the new entry within a few seconds.
- Open the Azure Portal → Change Feed blade for the container. The test document should appear in the list.
- Check the
ChangeFeedProcessor.*metrics in Azure Monitor forReadRequestsandExceptions. No spikes or errors indicate healthy operation.
Step 5 – Monitor Throughput and Diagnostics
Change Feed reads add a small RU cost (≈1 RU per 1 kB of data). For high‑change workloads, monitor the ReadRequests metric and adjust the container's throughput if you hit the RU/s limit. Use Azure Monitor alerts: ChangeFeedProcessor.ReadRequestCount > 80% of RU/s to pre‑empt throttling.
Enable diagnostic logs for the processor by adding processor.EnableDiagnostics() and routing logs to Log Analytics. Search for ChangeFeedProcessor.Exceptions to troubleshoot failures.
Step 6 – Fallback and Recovery Strategies
- Dead‑Letter Queue: Wrap your processing logic in a try/catch. On exception, serialize the failed change and enqueue it to an Azure Storage Queue for later inspection.
try { // process } catch (Exception ex) { var deadLetter = JsonSerializer.Serialize(item); await queueClient.SendMessageAsync(deadLetter); } - Retention Extension: If your processing pipeline may lag, increase
RetentionPeriodInHoursor useWithStartTime(DateTime.UtcNow.AddHours(-48))to catch up. - Lease Container Health: Periodically run a script that lists leases and checks for stale entries (leases older than lease timeout × 2). Re‑create the lease container if corruption is detected.
- Scaling Workers: The processor can run multiple instances across hosts. Each instance must use a unique
InstanceNameto avoid lease conflicts.
Expected Checks and Recovery Options
| Check | What to Verify | Recovery Action |
|---|---|---|
| Change Feed Enabled | Portal blade shows active feed and retention period. | Re‑enable via SDK or portal. |
| Lease Container Exists | Container present with correct partition key. | Delete and recreate. |
| Processor Logs | At least one change processed within 5 s of insertion. | Check network latency; adjust LeaseContainerLeaseTimeInSeconds if needed. |
| RU/s Consumption | ReadRequests < 80 % of provisioned RU/s. | Increase throughput or batch size. |
| Dead‑Letter Queue | Zero messages after successful run. | Investigate processing exceptions; add idempotency. |
Limitations & Practical Check
- The default retention period is 24 h; if your pipeline cannot keep up, data will be lost unless you extend retention.
- Large containers with frequent changes can generate thousands of batches per minute; design your processor to handle back‑pressure and consider scaling out.
- Change Feed is not a full event store – it only records the latest state. If you need historical snapshots, consider a separate change‑log collection.
Practical check: After deploying, insert a document and watch the processor console. If the document never appears, run az cosmosdb collection show --name <container> to confirm the change feed is active and that the lease container is healthy.
Conclusion
By enabling Change Feed, setting up a lease container, and consuming changes with the processor library, you transform Cosmos DB into a reliable event source. The key to success is monitoring retention, throughput, and lease health, and having a dead‑letter path for failures. With these safeguards, the change feed can power scalable, event‑driven architectures without the overhead of continuous polling.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.