Cosmos DB Change Feed with Azure Functions: What to Expect Before You Wire It Up
The change feed is always on for SQL API containers, so the real work is designing for per-partition ordering and at-least-once delivery. Here is a concrete Azure Functions setup and how to verify it.
25 Feb 2026, 02:34 UTC

The problem: keeping a second store in step without polling
A common requirement is to react to writes in a Cosmos DB container — push a document to a search index, fan out a notification, or append to an audit store. Polling with a timestamp filter works, but it costs request units (RUs, the throughput currency Cosmos DB bills against) on every poll and adds a delay equal to your polling interval.
The change feed is the built-in alternative: a persistent, per-partition record of changes that a consumer reads as a stream. The practical takeaway is that the hard part is not turning it on — for the SQL API it is already available on every container — but designing the consumer around two properties: ordering is per logical partition, and delivery is at least once.
What the change feed actually guarantees
- Scope: changes are ordered within a logical partition key value. There is no global ordering across partitions, so two documents with different partition keys can arrive in any relative order.
- Delivery: the Azure Functions trigger is built on the change feed processor and is at-least-once. A restart or a failure mid-batch can replay documents, so handlers must be idempotent.
- Latency: the feed is eventually consistent with the write. Expect a short delay, not a synchronous callback.
- Cost: reading the feed consumes RUs on the monitored container, and the lease container consumes its own RUs.
One correction worth stating plainly: there is no "enable change feed" switch for the standard mode on the SQL API. It is on by default. A separate mode that also captures deletes exists, but it is configured through a container-level change feed policy with a retention duration, and its API and region support should be checked against current documentation before you depend on delete events.
A worked example: container to Azure Function
The example below assumes Azure Functions runtime v4 with the .NET isolated worker model and the Microsoft.Azure.Functions.Worker.Extensions.CosmosDB package. Run the CLI commands from an authenticated Azure CLI session with permission to create resources in the target resource group.
Create the monitored container. Note that no change-feed flag appears here:
az cosmosdb sql container create \
--resource-group <rg> \
--account-name <account> \
--database-name <db> \
--name <container> \
--partition-key-path "/tenantId" \
--throughput 400Point the function at it. The connection string setting needs read access to both the monitored container and the lease container, which stores the checkpoint position for each partition range.
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
public class ChangeFeedHandler
{
private readonly ILogger<ChangeFeedHandler> _logger;
public ChangeFeedHandler(ILogger<ChangeFeedHandler> logger) => _logger = logger;
[Function("ChangeFeedHandler")]
public void Run(
[CosmosDBTrigger(
databaseName: "%CosmosDbName%",
containerName: "%CosmosContainerName%",
Connection = "CosmosDbConnection",
LeaseContainerName = "leases",
CreateLeaseContainerIfNotExists = true)]
IReadOnlyList<MyDocument> changes)
{
if (changes is null || changes.Count == 0) return;
foreach (var doc in changes)
{
// At-least-once: make this operation safe to repeat.
_logger.LogInformation("Changed {Id} in partition {Pk}", doc.id, doc.TenantId);
}
}
}Set the connection string as an application setting rather than committing it to source control:
az functionapp config appsettings set \
--resource-group <rg> \
--name <funcapp> \
--settings CosmosDbConnection="<connection-string>"Checking that it works
- Write a document to the container through the SDK or Data Explorer.
- Confirm the function logged the document id. Locally, this appears in the
func startconsole; in Azure, use the log stream or Application Insights. - Open the lease container and inspect a lease document. It should show an owner and a continuation position that advances after each batch. If it never advances, the trigger is not checkpointing.
- Write two updates to the same partition key value in quick succession and confirm they arrive in that order within the same batch or consecutive batches.
If the function never fires, check the connection string, confirm the lease container exists, and verify the function app can reach the account over the network — a private endpoint or firewall rule is a common cause.
Where it gets awkward
The two properties above drive most design decisions. Because ordering is per partition, a downstream system that needs a global sequence must impose it itself, for example by sorting on a version field before applying changes. Because delivery is at least once, every handler needs a natural idempotency key — writing with upsert on the document id is usually simpler than tracking processed ids in a separate store.
Throughput is the other constraint. A burst of writes produces a burst of feed reads, and if the container is provisioned at a low RU level the consumer will see throttling alongside the write workload. Monitor normalized RU consumption on both the monitored and lease containers, and remember that the lease container is a real container with its own cost.
What to do next
- Decide whether your handler can tolerate replay. If not, add an idempotency key before shipping.
- Choose the partition key with the change feed in mind, since it defines the ordering boundary your consumer sees.
- Start with a small batch size and increase it only after measuring handler duration.
- Alert on lease continuation lag so a stalled consumer is visible before downstream data goes stale.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.