← Files Azure Cosmos DBARCHIVED FILE

skills/cosmosdb-best-practices/rules/monitoring-ru-consumption.md

7.68 KB · Oct 3, 2026 · 06:21 UTC

↓ Download file

---
title: Track RU Consumption
impact: MEDIUM
impactDescription: enables cost optimization
tags: monitoring, ru, metrics, cost
---

## Track RU Consumption

Monitor Request Unit (RU) consumption to optimize costs and identify inefficient operations. Every operation has an RU cost.

**Incorrect (ignoring RU consumption):**

```csharp
// Operations without tracking cost
public async Task<Order> GetOrder(string orderId, string customerId)
{
    // No visibility into cost
    return await _container.ReadItemAsync<Order>(orderId, new PartitionKey(customerId));
    // Is this costing 1 RU or 100 RU? Unknown!
}
```

**Correct (tracking RU at operation level):**

```csharp
public async Task<Order> GetOrder(string orderId, string customerId)
{
    var response = await _container.ReadItemAsync<Order>(orderId, new PartitionKey(customerId));
    
    // Log RU consumption
    _logger.LogDebug(
        "Read order {OrderId}: {RU} RU, {Latency}ms",
        orderId,
        response.RequestCharge,
        response.Diagnostics.GetClientElapsedTime().TotalMilliseconds);
    
    // Track in metrics/telemetry
    _telemetry.TrackMetric("CosmosDB.ReadItem.RU", response.RequestCharge, 
        new Dictionary<string, string> 
        { 
            { "Operation", "ReadItem" },
            { "Container", "orders" }
        });
    
    return response.Resource;
}
```

```csharp
// Track RU for queries (can be high!)
public async Task<List<Order>> GetCustomerOrders(string customerId)
{
    var query = new QueryDefinition("SELECT * FROM c WHERE c.status = @status")
        .WithParameter("@status", "active");
    
    var totalRU = 0.0;
    var results = new List<Order>();
    
    var iterator = _container.GetItemQueryIterator<Order>(
        query,
        requestOptions: new QueryRequestOptions 
        { 
            PartitionKey = new PartitionKey(customerId),
            PopulateIndexMetrics = true  // Also get index metrics
        });
    
    while (iterator.HasMoreResults)
    {
        var response = await iterator.ReadNextAsync();
        results.AddRange(response);
        totalRU += response.RequestCharge;
        
        // Log per-page RU
        _logger.LogDebug(
            "Query page: {Count} items, {RU} RU, Index: {IndexMetrics}",
            response.Count,
            response.RequestCharge,
            response.IndexMetrics);
    }
    
    // Log total query cost
    _logger.LogInformation(
        "GetCustomerOrders: {Total} items, {TotalRU} total RU",
        results.Count,
        totalRU);
    
    // Alert on expensive queries
    if (totalRU > 100)
    {
        _logger.LogWarning(
            "Expensive query detected: {TotalRU} RU for {Count} items",
            totalRU, results.Count);
    }
    
    return results;
}
```

```csharp
// Middleware to track all operations
public class CosmosDbMetricsHandler : RequestHandler
{
    private readonly IMetricTracker _metrics;
    
    public override async Task<ResponseMessage> SendAsync(
        RequestMessage request, 
        CancellationToken cancellationToken)
    {
        var sw = Stopwatch.StartNew();
        var response = await base.SendAsync(request, cancellationToken);
        sw.Stop();
        
        _metrics.TrackDependency(
            "CosmosDB",
            request.RequestUri.ToString(),
            sw.Elapsed,
            response.IsSuccessStatusCode,
            new Dictionary<string, string>
            {
                { "RU", response.Headers.RequestCharge.ToString() },
                { "StatusCode", response.StatusCode.ToString() }
            });
        
        return response;
    }
}

// Register handler
var client = new CosmosClient(connectionString, new CosmosClientOptions
{
    CustomHandlers = { new CosmosDbMetricsHandler(_metrics) }
});
```

### Node.js / TypeScript (@azure/cosmos v4)

Every `@azure/cosmos` operation exposes `requestCharge` as a top-level numeric property on the response. Capture it on every call — point reads, queries, writes, and bulk operations.

**Incorrect (discarding requestCharge — no visibility into cost):**

```typescript
// ❌ requestCharge available but never captured
const { resource } = await container.item(orderId, userId).read();
return resource;
// Is this costing 1 RU or 100 RU? Unknown!
```

**Correct (capturing requestCharge on reads and writes):**

```typescript
import { Container, FeedResponse } from '@azure/cosmos';

// ✅ Point read — capture requestCharge
export async function getOrder(container: Container, id: string, userId: string) {
  const response = await container.item(id, userId).read();
  logger.debug({
    op: 'ReadItem',
    container: container.id,
    ru: response.requestCharge,
    statusCode: response.statusCode,
    activityId: response.activityId,
  }, 'cosmos.readItem');
  return response.resource;
}

// ✅ Write — create/upsert/replace/patch/delete all expose requestCharge
export async function createOrder(container: Container, order: Order) {
  const response = await container.items.create(order);
  logger.debug({ op: 'CreateItem', ru: response.requestCharge }, 'cosmos.createItem');
  return response.resource;
}
```

**Correct (accumulating RU across query pages — single-page tracking undercounts paged results):**

```typescript
// ✅ Query — sum requestCharge across all pages
export async function getCustomerOrders(container: Container, userId: string) {
  const iterator = container.items.query<OrderSummary>({
    query: 'SELECT c.id, c.userId, c.status, c.total, c.createdAt FROM c WHERE c.userId = @u ORDER BY c.createdAt DESC',
    parameters: [{ name: '@u', value: userId }],
  }, { partitionKey: userId });

  const results: OrderSummary[] = [];
  let totalRU = 0;

  while (iterator.hasMoreResults()) {
    const page: FeedResponse<OrderSummary> = await iterator.fetchNext();
    results.push(...page.resources);
    totalRU += page.requestCharge;
  }

  logger.info({ op: 'Query', container: container.id, count: results.length, totalRU }, 'cosmos.query.total');
  if (totalRU > 100) {
    logger.warn({ totalRU, count: results.length }, 'cosmos.query.expensive');
  }
  return results;
}
```

**`requestCharge` API surface in `@azure/cosmos` v4:**

| Operation | Response type | RU property |
|-----------|---------------|-------------|
| `container.item(id, pk).read()` | `ItemResponse<T>` | `response.requestCharge` |
| `container.items.create(doc)` | `ItemResponse<T>` | `response.requestCharge` |
| `container.items.upsert(doc)` | `ItemResponse<T>` | `response.requestCharge` |
| `container.item(id, pk).replace(doc)` | `ItemResponse<T>` | `response.requestCharge` |
| `container.item(id, pk).patch(ops)` | `ItemResponse<T>` | `response.requestCharge` |
| `container.item(id, pk).delete()` | `ItemResponse<T>` | `response.requestCharge` |
| `container.items.query(...).fetchAll()` | `FeedResponse<T>` | `response.requestCharge` |
| `container.items.query(...).fetchNext()` | `FeedResponse<T>` per page | sum across pages |
| `container.items.bulk(ops)` | `OperationResponse[]` | `op.requestCharge` per operation |

Azure Monitor queries for RU analysis:
```kusto
// Top expensive operations
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.DOCUMENTDB"
| summarize TotalRU = sum(requestCharge_s) by OperationName
| order by TotalRU desc

// RU per partition key (detect hot partitions)
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.DOCUMENTDB"
| summarize TotalRU = sum(requestCharge_s) by partitionKey_s
| order by TotalRU desc
```

Reference: [Monitor RU/s](https://learn.microsoft.com/azure/cosmos-db/monitor-request-unit-usage)

SHA-256: 0acb7fc3c191c6e64f50276855e1693cd9a150379cc88b20f3005c71e20ce0d0