The Cosmos DB Change Feed doesn’t actually "process" changes in real-time; it’s a persistent log of historical changes that you then process.

Let’s see it in action. Imagine we have a products container in Cosmos DB. When a product’s price changes, we want to update a separate inventory container.

Here’s a sample products document:

{
    "id": "prod-123",
    "name": "Wireless Mouse",
    "price": 25.99,
    "category": "Electronics",
    "_ts": 1678886400
}

And here’s how we’d set up a Change Feed processor using Azure Functions. The core idea is that Azure Functions (or any other host) can read from the Change Feed.

First, we need a function that triggers on Change Feed events. This is typically done with a CosmosDBTrigger binding.

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Azure.Documents;
using System.Collections.Generic;
using System.Threading.Tasks;

public static class ChangeFeedProcessor
{
    [FunctionName("ChangeFeedTrigger")]
    public static async Task Run(
        [CosmosDBTrigger(
            databaseName: "myDatabase",
            collectionName: "products", // The source container
            ConnectionStringSetting = "CosmosDBConnection",
            LeaseCollectionName = "leases", // A separate container for lease management
            CreateLeaseCollectionIfNotExists = true)]IReadOnlyList<Document> input,
        TraceWriter log)
    {
        if (input != null && input.Count > 0)
        {
            log.Info($"Documents modified: {input.Count}");
            foreach (var doc in input)
            {
                log.Info($"Processing document ID: {doc.Id}");
                // Here's where you'd add your processing logic.
                // For example, updating another container.
                // Let's simulate updating an inventory count.
                var productId = doc.Id;
                var newPrice = (double)doc.GetPropertyValue<double>("price");
                log.Info($"Product {productId} new price is {newPrice}");

                // In a real scenario, you'd interact with another Cosmos DB client
                // to update or insert into the 'inventory' container.
                // Example: await inventoryService.UpdateInventoryPrice(productId, newPrice);
            }
        }
    }
}

The CosmosDBTrigger binding handles the complexity of reading the Change Feed. It uses a leases collection to keep track of which documents have been processed by which instance of your function, ensuring exactly-once processing. The ConnectionStringSetting points to your Cosmos DB account.

The mental model here is that the Change Feed is an append-only log. When a document is created, updated, or deleted in the source container (products in this case), a record of that change is added to its Change Feed. Your processor (the Azure Function) then polls this feed for new entries. The leases collection is crucial; it allows multiple instances of your function to scale out and process the feed concurrently without duplicating work. Each lease entry represents a partition of your source container, and a worker (an instance of your function) claims a lease to process that partition.

The key levers you control are:

  1. The Source Container: Which container’s changes you want to capture.
  2. The Lease Collection: A dedicated container to store lease information. This must exist or be configured to be created.
  3. The Trigger Logic: The code within your function that reacts to the changes. This could be anything from updating another container, sending a message to a queue, calling an external API, or performing complex aggregations.
  4. Batch Size and Timeouts: While not explicitly in the trigger attribute above, you can configure the MaxItemsPerInvocation and FeedPollDelay (implicitly) to control how often and how much data your processor reads at once.

The most surprising thing about the Change Feed is that it doesn’t require a separate "enable Change Feed" switch. It’s an inherent feature of every container in Cosmos DB, always active, always logging. You only need to opt-in to reading it via a processor.

The _ts field on your documents isn’t directly used by the Change Feed mechanism itself, but it’s a timestamp that’s updated on every modification. The Change Feed internally uses a different, more granular versioning mechanism to track changes across partitions.

After you’ve successfully processed all current changes and your processor is running, the next thing you’ll encounter is dealing with potential duplicate events during network interruptions or restarts, which requires idempotent processing logic.

Want structured learning?

Take the full Cosmos-db course →