Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Azure Cosmos DB delivers single-digit millisecond reads and writes at any scale. When the same items are read thousands of times per second, or traffic spikes on a few hot keys, a cache serves those repeated reads from memory and keeps hot partitions from throttling.
This article shows how to use Azure Managed Redis as a cache for an Azure Cosmos DB workload. It explains how to choose a caching pattern, and then implements four of them in Python with the azure-cosmos SDK, redis-py, and Microsoft Entra ID authentication, using an online retail store as the example.
When to add a cache
A cache is most useful when your application repeatedly reads the same items or query results, especially when that traffic concentrates on a few items. A container's provisioned throughput is divided evenly across its physical partitions. During a flash sale, repeated reads of popular products can exhaust their partition's share, causing 429 errors even while other partitions have spare capacity. A cache protects these hot partitions by serving repeated reads without sending them to the database.
Serving repeated reads from the cache also reduces request unit (RU) consumption, which frees provisioned throughput for writes and unique queries. This benefit can let you lower your autoscale maximum or reduce serverless consumption.
Don't add a cache, and read directly from Azure Cosmos DB, in these cases:
- Reads rarely repeat. Few reads would be served from the cache.
- The workload is write-heavy. Most items change before they're read again, so cached entries are rarely used.
- Reads use strong, bounded staleness, or consistent prefix consistency. A cache can't keep the guarantees these levels give. Strong returns the latest committed write, bounded staleness limits how far behind a read can be, and consistent prefix never shows writes out of order. These reads automatically bypass the integrated cache. Azure Managed Redis has no such bypass, so your code must send these reads directly to Azure Cosmos DB instead of to the cache.
Choose between the integrated cache and an external cache
Azure Cosmos DB offers an integrated cache, or you can add an external cache such as Azure Managed Redis. Both options serve cache hits without consuming RUs, but they differ in what they cache and how you configure and manage them.
The Azure Cosmos DB integrated cache caches point reads and query results without requiring application caching code. It isn't turned on by default. To use it, provision a dedicated gateway and configure your application to connect through it in gateway mode.
Azure Managed Redis is an in-memory data store that your application uses as an external cache in front of Azure Cosmos DB. You control what to cache, how long to keep it, and how to refresh or invalidate it. It can cache database results or application-generated data and provides data structures such as sorted sets for rankings.
Use the following table to choose the option that fits your workload.
| Concept | Rely on the integrated cache when | Add an external cache (Azure Managed Redis) when |
|---|---|---|
| What you cache | You want to cache the point reads and queries your application already sends. | You want to cache something your application builds, such as a rendered listing page, an aggregate, or a ranking, or data that combines Azure Cosmos DB with other sources. You also need data structures that Azure Cosmos DB doesn't provide, such as sorted sets for rankings, atomic counters, rate limiters, or shopping carts and sessions. |
| Caching code | You don't want to write caching code. The integrated cache needs only configuration: provision a dedicated gateway, then change your client's connection settings to use its endpoint and gateway mode. | You want to write and maintain the cache logic yourself, including what to cache, which keys and expiry times to use, and how to update or delete entries when data changes. The patterns in this article show this code. |
| Freshness | Your data can be slightly out of date. The integrated cache isn't notified when an item changes. Writes through the same gateway node update its cached items but not its cached query results. So freshness depends on the staleness limit you set for each read request. A longer limit gives more cache hits but older data. | Users must see changes quickly, such as a price update, including changes made by other services, imports, or admin tools. Your code updates or deletes the cached entry when the data changes, so entries can stay cached for a long time and still be current. |
| Size and sharing | Your frequently read items fit within each gateway node's cache, which is about 8 to 32 GB depending on the node size. Each node in each region keeps its own copy, so each node pays RUs the first time it loads an item. | Your frequently read data doesn't fit in one gateway node's cache, or you want all your services to share one copy. When one service caches an item, the other services get a cache hit on it instead of reading it from Azure Cosmos DB. Azure Managed Redis instances range from 0.5 GB to 4.5 TB. |
These two cache options can also work together. For example, let the integrated cache serve the point reads and queries that repeat, and use Azure Managed Redis for the listing pages, rankings, and shopping carts that your application builds. If you use both, watch for item freshness. When a Redis entry expires, your code reads from Azure Cosmos DB to fill it again. If that read goes through the integrated cache, it can return an older copy of the data, and Redis keeps that older copy until the entry expires again. To avoid this problem, send these refill reads with a short staleness limit, or bypass the integrated cache.
Both options add a provisioned resource that runs alongside Azure Cosmos DB. Weigh it against the throughput the cache frees, and validate that the remaining throughput can support cache misses and cache outages.
Caching patterns
To understand the various caching patterns, consider an online retail store as a running example. Each store feature reads and writes data differently, so each feature requires its own caching pattern. The following table pairs each pattern with a store feature and explains the benefit for Azure Cosmos DB, using Azure Managed Redis as the cache.
| Pattern | How it works | In the online retail store | Why it fits Azure Cosmos DB |
|---|---|---|---|
| Cache-aside | The application checks the cache first. On a miss, it reads from Azure Cosmos DB, then populates the cache with a time to live (TTL). | Category pages and popular products. | Queries consume more RUs than single-item reads. Caching a busy category page avoids rerunning the same query for every shopper and takes load off hot partitions. |
| Write-through | The application saves each change to the database first. After the write succeeds, it writes the new value to the cache. Reads then see the change right away instead of after the TTL expires. | Price and inventory updates. | When the application updates an item, such as a product's price, Azure Cosmos DB can return the complete updated item in the response. The application writes that item to the cache directly, so it doesn't spend RUs to read the item back. If the write fails, the cache isn't touched. |
| Event-driven refresh | When any writer changes the data, a change event runs a function that writes the new value to the matching cache entry. The cache stays current even for changes that your application didn't make. | Order status updated by the warehouse's fulfillment system, or any change from other services, imports, or admin tools. | The change feed reports the latest version of each inserted or updated item, no matter which app or tool made it. The Azure Functions trigger for Azure Cosmos DB reads it and refreshes the cached entry, typically within seconds. |
| Sorted-set ranking | A Redis sorted set keeps a running total of units sold per product, updated as orders arrive. | Best-sellers list. | Without a cache, the list needs a query that totals every order across all partitions on each page view. With the cache, the page reads the top products from Redis and costs no RUs. |
| Reference-data prefetching | Stable data is loaded into the cache up front and updated when the source changes, instead of on a miss. | Shipping options and store settings. | For small, frequently reused datasets, loading them into a shared cache avoids repeated Azure Cosmos DB reads across application instances. Prefetching also avoids a cache miss on the first request. Choose a refresh strategy based on how often the data changes and how much staleness the application can tolerate. |
| Session and state offload | Transient state lives in the cache instead of the database until it becomes permanent, such as an order at checkout. | Shopping carts and user sessions. | Carts change on every click, and many are abandoned. Writing only the final order to Azure Cosmos DB avoids a database write for every click. Use this pattern only when losing the state is acceptable, or save a snapshot to Azure Cosmos DB. |
| Multi-region caching | A cache in each region serves local reads, kept in sync with active geo-replication. | Shoppers in multiple regions. | Shoppers read cached data from a nearby region. With global distribution, and preferred locations set in the client, cache misses also read from a nearby database replica. |
Prerequisites
- An Azure account with an active subscription. Create an account for free.
- An Azure Cosmos DB account that uses the NoSQL API. To create one, see Create an Azure Cosmos DB account. If the account firewall is turned on, allow your client IP address so that you can connect from your machine.
- The sample database (
retail) and the containers that the pattern examples use:products(partition key/categoryId),orders(/customerId), andleases(/id). The sample app creates the database and containers and adds data withazd up. If you create them yourself, use a control-plane tool, such as the Azure portal or the Azure CLI, because data-plane roles can't create databases or containers. - An Azure Managed Redis instance. To create one, see Create an Azure Managed Redis instance. Create it in the same region as your application and Azure Cosmos DB account to minimize latency. On the Advanced tab, set Clustering policy to Enterprise, which the code in this article uses. You can't change this policy after you create the cache. For more information, see Choose a clustering policy.
- Data access for your developer or application identity on both services: the Cosmos DB Built-in Data Contributor role on the account, and a Redis access policy assignment on the cache. For Azure Cosmos DB, see Connect by using role-based access control and Microsoft Entra ID. For Redis, see Use Microsoft Entra ID for authentication with Azure Managed Redis. The sample app grants this access for you. In production, grant your managed identities only the access they need, through your deployment pipeline.
- Python 3.10 or later.
- The Azure CLI. Sign in with
az loginso thatDefaultAzureCredentialcan use your identity. - For event-driven refresh, a function app that runs Python and has a managed identity, and Azure Functions Core Tools to publish the code.
Tip
For the complete, deployable version of this sample, including infrastructure as code, see cosmos-db-retail-sample in the amr-caching-pattern-samples repository on GitHub. The sample also includes reference-data prefetching and session offload.
Implement the caching patterns
The first two steps install the client libraries and connect to both services. The remaining steps show code for four patterns in the Caching patterns table: cache-aside, write-through, event-driven refresh, and sorted-set ranking. The event-driven refresh example includes a function that runs in Azure Functions.
Step 1: Install the client libraries
Install the Azure Cosmos DB and Redis client libraries, along with the Azure Identity library for Microsoft Entra authentication and redis-entraid for Redis token renewal.
pip install "azure-cosmos>=4.9,<5.0" "redis>=5.3,<9.0" "redis-entraid>=1.0,<2.0" "azure-identity>=1.17,<2.0"
Step 2: Connect to Azure Cosmos DB and Azure Managed Redis
Use Microsoft Entra ID authentication instead of access keys. Microsoft Entra ID removes the need to store secrets in your application and lets you manage access centrally.
The following code creates an Azure Cosmos DB client and a Redis client. Both authenticate through DefaultAzureCredential, which uses the application's managed identity when the code runs in Azure, and your developer credential, such as your Azure CLI sign-in, when it runs on your machine.
Because the cache uses the Enterprise clustering policy, the code connects to a single endpoint with the standard redis.Redis client. The client verifies the server certificate and checks that it matches the cache hostname, which keeps another server from posing as your cache and receiving your Microsoft Entra token. The OSS clustering policy redirects clients to nodes by IP address, which the certificate doesn't list, so cluster clients must turn off the hostname check. If you use the OSS policy, connect only through a private endpoint.
Expand to view the connection code (24 lines)
import os
import redis
from azure.cosmos import CosmosClient
from azure.identity import DefaultAzureCredential
from redis_entraid.cred_provider import create_from_default_azure_credential
credential = DefaultAzureCredential()
cosmos = CosmosClient(
os.environ["COSMOS_ENDPOINT"], # https://<account>.documents.azure.com:443/
credential=credential,
)
db = cosmos.get_database_client("retail")
products = db.get_container_client("products")
orders = db.get_container_client("orders")
cache = redis.Redis(
host=os.environ["REDIS_HOST"], # <name>.<region>.redis.azure.net
port=10000,
ssl=True,
ssl_check_hostname=True, # The certificate must match the cache hostname.
credential_provider=create_from_default_azure_credential(("https://redis.azure.com/.default",)),
decode_responses=True,
)
Note
Microsoft Entra tokens expire, typically after about one hour. The Azure Cosmos DB SDK requests new tokens automatically. For Redis, the redis-entraid credential provider gets tokens and reauthenticates open connections before each token expires.
Step 3: Cache category pages and popular items with cache-aside
Cache-aside is the most common caching pattern. With cache-aside, the application checks Redis first, reads from Azure Cosmos DB only on a miss, and then populates the cache with a TTL. A few popular categories and products drive most reads, so the hit rate is high.
Because most reads are served from memory, fewer requests reach Azure Cosmos DB. This pattern frees provisioned throughput, so you can absorb read spikes without scaling up. To identify the operations worth caching, see Find what to cache.
Cache query results
A category page shows the top-rated products in a category. The query is scoped to one partition, but it still reads and sorts many items and costs far more than a point read. Cache the result.
Expand to view the category page code (28 lines)
import json
import random
CATEGORY_TTL = 120 # seconds
def get_category_page(category_id: str) -> list[dict]:
key = f"category:{category_id}:top"
# 1. Try the cache first.
cached = cache.get(key)
if cached is not None:
return json.loads(cached)
# 2. On a miss, query Azure Cosmos DB in one partition.
items = list(products.query_items(
query=(
"SELECT c.id, c.categoryId, c.name, c.price, c.rating, c.imageUrl "
"FROM c WHERE c.categoryId = @categoryId "
"ORDER BY c.rating DESC OFFSET 0 LIMIT 24"
),
parameters=[{"name": "@categoryId", "value": category_id}],
partition_key=category_id,
))
# 3. Populate the cache with a TTL, then return.
ttl = CATEGORY_TTL + random.randint(0, 30) # jitter avoids synchronized expiry
cache.set(key, json.dumps(items), ex=ttl)
return items
Scope cached queries to a single partition, and project only the properties the page needs. A single-partition query costs fewer RUs on a cache miss, and a smaller result uses less memory in Redis.
To see the effect, consider a category query that costs 12 RUs and runs 1,500 times per second. That's 18,000 RU/s on one partition key value, more than a single physical partition can serve. With a 95% cache hit rate, Azure Cosmos DB would only have to serve 75 queries per second, consuming 900 RU/s.
Cache popular items
Point reads are the cheapest reads in Azure Cosmos DB, about 1 RU for a 1-KB item. But every read of a popular item goes to the same partition, so an item that suddenly gets heavy traffic, such as a product in a flash sale, can overload that partition and cause 429 errors even when the container has spare throughput. Cache the items that receive a disproportionate share of reads, so most of those reads never reach the partition.
Expand to view the popular item code (34 lines)
from azure.cosmos.exceptions import CosmosResourceNotFoundError
PRODUCT_TTL = 300 # seconds
NOT_FOUND_TTL = 30 # seconds
SYSTEM_PROPERTIES = ("_rid", "_self", "_etag", "_attachments", "_ts", "_lsn")
def clean(item: dict) -> dict:
return {k: v for k, v in item.items() if k not in SYSTEM_PROPERTIES}
def get_product(category_id: str, product_id: str) -> dict | None:
# Include the partition key in the cache key, so a miss can fall back to a point read.
key = f"product:{category_id}:{product_id}"
# 1. Try the cache first.
cached = cache.get(key)
if cached is not None:
return json.loads(cached) # "null" means the product doesn't exist.
# 2. On a miss, point-read the item, the cheapest read in Azure Cosmos DB.
try:
item = products.read_item(item=product_id, partition_key=category_id)
except CosmosResourceNotFoundError:
item = None
# 3. Cache "not found" briefly so that repeated requests for a missing ID don't cost RUs.
if item is None:
cache.set(key, json.dumps(None), ex=NOT_FOUND_TTL)
return None
# 4. Populate the cache with a TTL, then return.
product = clean(item)
ttl = PRODUCT_TTL + random.randint(0, 60) # jitter avoids synchronized expiry
cache.set(key, json.dumps(product), ex=ttl)
return product
Step 4: Keep the cache current with write-through
With cache-aside alone, a cached value stays stale until its TTL expires, even after the data changes. Write-through closes that gap: every time the application writes to Azure Cosmos DB, it also writes the new value to the cache. Use it for data that users expect to see change right away, such as prices and stock levels.
In the online retail store, use write-through when the application changes a price, so the next read shows the new price right away. If the change also affects other cached results, such as a category page that lists the price, delete them from the cache so the next read rebuilds them with the new price. Write-through covers only changes that your application makes.
Expand to view the write-through code (23 lines)
from azure.core import MatchConditions
def update_price(category_id: str, product_id: str, new_price: float, etag: str | None = None):
# 1. If the caller passes the item's _etag, fail the write when someone else changed it first.
kwargs = {}
if etag:
kwargs = {"etag": etag, "match_condition": MatchConditions.IfNotModified}
# 2. Save the new price in Azure Cosmos DB. Patch returns the full updated item.
updated = products.patch_item(
item=product_id,
partition_key=category_id,
patch_operations=[{"op": "set", "path": "/price", "value": new_price}],
**kwargs,
)
# 3. The write succeeded. Cache the updated product, and delete the category
# listing, which still shows the old price.
pipe = cache.pipeline(transaction=False)
pipe.set(f"product:{category_id}:{product_id}", json.dumps(clean(updated)),
ex=PRODUCT_TTL + random.randint(0, 60))
pipe.delete(f"category:{category_id}:top")
pipe.execute()
To keep concurrent writers from overwriting each other's changes, use optimistic concurrency.
Write-through updates the cache after the Azure Cosmos DB write succeeds, but the two operations aren't atomic. A failed cache update or a concurrent read can leave stale data in the cache.
Keep Azure Cosmos DB as the source of truth. If a cache update fails, retry the update or invalidate the key when connectivity is restored. Retain a TTL as a fallback.
Step 5: Event-driven refresh with the Azure Functions trigger for Azure Cosmos DB
Write-through caching covers only changes your application makes. Other systems can change the same data directly in Azure Cosmos DB. When that happens, the cache still holds the old value.
For example, shoppers check whether their order ships. Each order in the orders container has an id, a customerId, which is the partition key, and a status. To make that check fast, the store caches each order's status in Redis, keyed by customer and order, using cache-aside with a TTL. When the warehouse ships an order, its system updates the order in Azure Cosmos DB directly. Your application doesn't know about the change, so shoppers keep seeing "placed" until the cached value expires.
Use this pattern when data changes outside your application and you can't wait for the cached value to expire.
To catch these changes, use the Azure Functions trigger for Azure Cosmos DB. The trigger reads the change feed of the orders container. Whenever an order changes, no matter who changed it, the trigger runs a function that writes the new status to the cache. The next time a shopper checks, the application reads "shipped" from the cache. Add the following function to a function app.
Expand to view the event-driven refresh code (26 lines)
# function_app.py
import azure.functions as func
app = func.FunctionApp()
# Invoked automatically when items in the orders container change
@app.cosmos_db_trigger(
arg_name="orders",
database_name="retail",
container_name="orders",
connection="CosmosDBConnection",
lease_container_name="leases",
create_lease_container_if_not_exists=False,
)
@app.retry(strategy="exponential_backoff", max_retry_count="5",
minimum_interval="00:00:04", maximum_interval="00:15:00")
def refresh_order_status(orders: func.DocumentList) -> None:
# The Redis client from Step 2, in shared/cache.py.
# Import it here, not at the top of the file, so the Functions host can start.
from shared.cache import cache
# Write each order's new status. xx=True skips orders that aren't cached,
# and keepttl=True keeps the original expiry.
pipe = cache.pipeline(transaction=False)
for order in orders:
pipe.set(f"order:{order['customerId']}:{order['id']}:status", order["status"], xx=True, keepttl=True)
pipe.execute()
The change feed delivers each changed order in full, including its new status. So refresh the cache by overwriting the cached status, not by deleting it. If you delete it, the next shopper's read misses the cache and goes to Azure Cosmos DB. Refresh only orders that are already in the cache. If no shopper has checked an order recently, it isn't cached, and adding it would only use memory. When there are no new changes, the trigger checks again every 5 seconds by default, so shoppers typically see the new status within a few seconds. Changes to the same order arrive in the order they happened. After an outage, the function resumes from its last checkpoint and catches up. If a Redis write fails, the trigger doesn't retry by default, so that update is lost. That's why the code sets a retry policy. Set one on each change feed function.
This refresh is best effort. If a read misses the cache just as the order changes, the read can cache the old status after the function has already skipped it. That old status lasts until its TTL expires, so a shorter TTL limits how long shoppers see it.
The function uses a managed identity to connect to Azure Cosmos DB and the cache. The trigger also needs a data-plane role to read the change feed and save its checkpoints in the leases container. The sample includes the complete setup.
Important
The trigger reads the change feed in latest version mode, which doesn't include deletes or TTL expirations. Orders change status but aren't deleted, so this example doesn't need them. For data that's deleted, mark items as deleted with a flag instead, and remove the cached entry when the function sees the flag.
Step 6: Build a best-sellers ranking with a sorted set
A best-sellers list needs to add up quantities across every order. Each order has a lines array, and each line has a productId, a categoryId, and a quantity. In Azure Cosmos DB, that's a cross-partition aggregation over the orders container, which you don't want to rerun on every page view. Instead, maintain the ranking incrementally from the change feed in a Redis sorted set.
Use this pattern for rankings and counters that can be approximate, such as a best-sellers widget. Keep financial totals and inventory in Azure Cosmos DB.
Each product is a sorted-set member, and its score represents units sold. As qualifying sales are processed, increment the corresponding scores. The storefront retrieves the highest-scoring products directly from Redis instead of recalculating totals from the orders container on every request.
Expand to view the best-sellers code (12 lines)
# Put this code inside the "for order in orders:" loop of its own change feed function.
# To create it, copy Step 5's function, or use update_bestsellers from the sample.
# Required: in the trigger, add lease_container_prefix="bestsellers-".
# Without it, this function and Step 5's split the order changes and each misses some.
# For each line in an order that hasn't been counted yet:
for line in order["lines"]:
# Include the category ID, the partition key of products, so the storefront
# can look up product details with cache-aside and a point read.
product_key = f"{line['categoryId']}:{line['productId']}"
cache.zincrby("bestsellers", line["quantity"], product_key)
In the storefront, read the ten products with the highest sales totals:
top_products = cache.zrange("bestsellers", 0, 9, desc=True, withscores=True)
For time-windowed rankings, such as best sellers this week, write to a separate key for each period, such as one per week, and set a TTL on it.
The change feed delivers an order again every time it changes, such as when the warehouse ships it, so count each order only once. A retry can also deliver the same batch again, so this check must hold across retries. A production consumer must also define how cancellations, returns, and ranking periods affect the totals. For a complete function, see function_app.py in the sample.
Best practices
- Cache the shape the page needs. Store the ready-to-use result the page shows, such as a category listing with only the properties it displays. One key lookup then replaces the whole query. Skip data that's rarely reused, because it wastes memory and pushes out useful keys. Cold point reads, which cost about 1 RU, are a common example.
- Use a consistent, versioned key naming scheme. Name each key after the data it holds, such as
product:{categoryId}:{id}, so your application and refresh logic can find the cached result. When the cached value's schema changes, add a version, such asproduct:v2:{categoryId}:{id}, so new code doesn't read entries written in the old format. Update the refresh logic to match. - Update the cache only after the write succeeds, and update or invalidate keys for external changes. If the Azure Cosmos DB write fails, leave the cache unchanged. For changes from other writers, refresh or delete affected keys so the next read gets current data. Retain a TTL as a fallback for missed updates.
- Don't let the cache outlive the item. Items that expire through Azure Cosmos DB TTL don't appear in the latest version change feed. For items that expire, cap the Redis TTL at the item's remaining lifetime,
_ts + ttl - nowin seconds. Use the item'sttlif it has one, otherwise the container's default TTL. If TTL isn't turned on for the container, or the effective TTL is -1, the item doesn't expire, so skip the cap. - Prevent stampedes. Add TTL jitter so that keys don't expire together. For very hot keys, let one request rebuild an expired entry, for example by taking a short lock with
SETand theNXandEXoptions, while other requests wait briefly and read the cache again. - Fall back gracefully. If Redis is unavailable, read from Azure Cosmos DB. Set short Redis timeouts, such as
socket_timeoutandsocket_connect_timeoutin redis-py, and use a circuit breaker so that a cache outage doesn't slow every request. Make sure your throughput can carry the load for a short period, for example by using autoscale. If Azure Cosmos DB returns 429 errors on a miss, return the last known value, for example from a longer-lived backup key, rather than failing the request. - Choose an eviction policy that fits your keys. This article mixes keys that have a TTL, such as product pages and category listings, with keys that don't, such as the best-sellers ranking. A
volatile-lrupolicy evicts only keys that have a TTL, which protects the ranking. If keys without a TTL fill the cache, Redis rejects writes with an out-of-memory error, so monitor the Used Memory Percentage metric. If a cache holds only cached query results, useallkeys-lruinstead. - Co-locate and secure resources. Put the application and the cache in the same Azure region as the Azure Cosmos DB region that the application reads from, to keep latency low. In production, connect to both services through private endpoints.
- Monitor both sides and right-size. Track the Redis hit ratio (from the Cache Hits and Cache Misses metrics), Evicted Keys, and Server Load alongside Azure Cosmos DB Total Request Units, Normalized RU Consumption, and Total Requests filtered to status code 429 to confirm that the cache is offloading work from the database. If evictions or server load stay high, scale the cache.
- Serialize efficiently. JSON is easy to read and debug. For high-throughput paths, test a compact binary format, and measure memory and latency before you switch.
Find what to cache
The best candidates are queries and reads that run often, use the most RUs in total, and return results that rarely change. Azure Cosmos DB provides this history in two places:
- Metrics are collected by default, with no setup. In the Azure portal, Total Request Units split by operation type and container shows where your RUs go, and Normalized RU Consumption split by partition key range shows hot partitions. Azure Monitor insights shows the same data as ready-made charts.
- Resource logs record the RU charge and text of each query, so you can find the specific queries that cost the most. They aren't collected by default: create a diagnostic setting that sends the DataPlaneRequests and QueryRuntimeStatistics categories to a Log Analytics workspace in resource-specific mode. Query text is obfuscated unless you also turn on Diagnostics full-text query on the account's Features page. This setting can add logging costs, so turn it off when you finish.
After the logs collect several days of normal traffic, the following query lists the queries that consumed the most RUs over the last seven days, with how many requests each one made. A query that returns several pages or reads several partitions makes more than one request. It skips the leases container, where the Azure Functions trigger stores its checkpoints. It needs read access to the workspace, such as the Log Analytics Reader role.
Expand to view the resource log query (10 lines)
CDBDataPlaneRequests
| where TimeGenerated > ago(7d) and OperationName == "Query" and CollectionName != "leases"
| join kind=inner (
CDBQueryRuntimeStatistics
| where TimeGenerated > ago(7d)
| distinct ActivityId, QueryText // One row per query; the table has a row for each partition the query touched
) on ActivityId
| summarize Requests = count(), TotalRU = sum(RequestCharge), AvgRequestRU = avg(RequestCharge)
by AccountName, DatabaseName, CollectionName, QueryText
| top 20 by TotalRU
Use metrics to find the containers and partitions under pressure, and resource logs to find the exact queries behind it. Prioritize queries that are frequent, expensive, and stable: a high request count, a high total RU charge, and results that don't change on every request. A query that runs constantly but returns the same items for minutes, such as a category listing, shipping options, or a best-sellers list, is an ideal candidate.