
Cosmosdb Best Practices
- 1 installs
- 2 repo stars
- Updated July 7, 2026
- azurecosmosdb/cosmosdb-claude-code-plugin
cosmosdb-best-practices is a Claude skill providing Azure Cosmos DB performance optimization guidelines for NoSQL modeling, partitioning, queries, SDK usage, and vector search.
About
cosmosdb-best-practices is a skill providing Azure Cosmos DB performance optimization and best-practice guidelines for NoSQL data modeling, partitioning, queries, SDK usage, and vector search. A developer uses it when writing, reviewing, or refactoring code that interacts with Azure Cosmos DB, or when designing data models and choosing partition keys. It packages 73+ rules across 10 categories as individual files loaded on demand.
- Provides 73+ Azure Cosmos DB performance rules across 10 categories, prioritized by impact
- Covers data modeling, partition key design, query optimization, SDK usage, and vector search
- Loads only the relevant per-rule file on demand, synced from the AzureCosmosDB agent-kit
Cosmosdb Best Practices by the numbers
- 1 all-time installs (skills.sh)
- Ranked #765 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
cosmosdb-best-practices capabilities & compatibility
- Capabilities
- database design · query optimization · data modeling · code review
- Works with
- azure
- Use cases
- database · code review · refactoring
- Pricing
- Free
What cosmosdb-best-practices says it does
Azure Cosmos DB performance optimization and best practices guidelines for NoSQL, partitioning, queries, SDK usage, and vector search.
containing 73+ rules across 10 categories, prioritized by impact to guide automated refactoring and code generation.
npx skills add https://github.com/azurecosmosdb/cosmosdb-claude-code-plugin --skill cosmosdb-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 2 |
| Last updated | July 7, 2026 |
| Repository | azurecosmosdb/cosmosdb-claude-code-plugin ↗ |
What it does
Design data models, partition keys, and queries for Azure Cosmos DB and review code for performance issues.
Who is it for?
Designing Cosmos DB data models, choosing partition keys, optimizing queries, and reviewing SDK code for performance.
Skip if: Relational databases, or NoSQL systems other than Azure Cosmos DB.
When should I use this skill?
Writing, reviewing, or refactoring code that interacts with Azure Cosmos DB, designing data models, or optimizing queries.
What you get
Cosmos DB data models, partition keys, and queries that follow prioritized best-practice rules for performance and scale.
- Cosmos DB data model guidance
- partition-key recommendations
- optimized queries and SDK code
By the numbers
- 73+ rules across 10 categories
- keep items well under the 2MB item limit
- 20GB logical partition limit
Files
Azure Cosmos DB Best Practices
Comprehensive performance optimization guide for Azure Cosmos DB applications, containing 73+ rules across 10 categories, prioritized by impact to guide automated refactoring and code generation.
73 individual rule files are in the rules/ directory, one file per rule, synced from the AzureCosmosDB/cosmosdb-agent-kit. Load only the relevant rule file(s) when answering a question — do NOT load all files at once. Run /azure-cosmosdb:generate-skills to sync with the latest rules from the agent-kit.
When to Apply
Reference these guidelines when:
- Designing data models for Cosmos DB
- Choosing partition keys
- Writing or optimizing queries
- Implementing SDK patterns
- Reviewing code for performance issues
- Configuring throughput and scaling
- Building globally distributed applications
- Implementing vector search and RAG patterns
Rule Index
Rules are grouped by category prefix. For a given question, load files matching the relevant prefix (e.g., model-*.md for data modeling, sdk-*.md for SDK usage). See rules/_sections.md for category descriptions.
1. Data Modeling — CRITICAL (prefix: model-)
- model-embed-related.md — Embed related data retrieved together
- model-reference-large.md — Reference data when items grow large
- model-avoid-2mb-limit.md — Keep items well under 2MB limit
- model-id-constraints.md — Follow ID value length and character constraints
- model-nesting-depth.md — Stay within 128-level nesting depth limit
- model-numeric-precision.md — Understand IEEE 754 numeric precision limits
- model-denormalize-reads.md — Denormalize for read-heavy workloads
- model-schema-versioning.md — Version your document schemas
- model-type-discriminator.md — Use type discriminators for polymorphic data
- model-json-serialization.md — Handle JSON serialization correctly
- model-relationship-references.md — Use ID references with transient hydration
2. Partition Key Design — CRITICAL (prefix: partition-)
- partition-high-cardinality.md — Choose high-cardinality partition keys
- partition-avoid-hotspots.md — Distribute writes evenly
- partition-hierarchical.md — Use hierarchical partition keys for flexibility
- partition-query-patterns.md — Align partition key with query patterns
- partition-synthetic-keys.md — Create synthetic keys when needed
- partition-key-length.md — Respect partition key value length limits
- partition-20gb-limit.md — Plan for 20GB logical partition limit
3. Query Optimization — HIGH (prefix: query-)
- query-avoid-cross-partition.md — Minimize cross-partition queries
- query-use-projections.md — Project only needed fields
- query-pagination.md — Use continuation tokens for pagination
- query-avoid-scans.md — Avoid full container scans
- query-parameterize.md — Use parameterized queries
- query-order-filters.md — Order filters by selectivity
4. SDK Best Practices — HIGH (prefix: sdk-)
- sdk-singleton-client.md — Reuse CosmosClient as singleton
- sdk-async-api.md — Use async APIs for throughput
- sdk-retry-429.md — Handle 429s with retry-after
- sdk-connection-mode.md — Use Direct mode for production
- sdk-preferred-regions.md — Configure preferred regions
- sdk-excluded-regions.md — Exclude regions experiencing issues
- sdk-availability-strategy.md — Configure availability strategy for resilience
- sdk-circuit-breaker.md — Use circuit breaker for fault tolerance
- sdk-diagnostics.md — Log diagnostics for troubleshooting
- sdk-serialization-enums.md — Serialize enums as strings not integers
- sdk-emulator-ssl.md — Configure SSL and connection mode for Cosmos DB Emulator
- sdk-etag-concurrency.md — Use ETags for optimistic concurrency
- sdk-java-content-response.md — Enable content response on write operations (Java)
- sdk-java-cosmos-config.md — Configure Cosmos DB in Spring Boot with dependent beans
- sdk-java-spring-boot-versions.md — Match Java version to Spring Boot requirements
- sdk-local-dev-config.md — Configure local development to avoid cloud conflicts
- sdk-newtonsoft-dependency.md — Explicitly reference Newtonsoft.Json package (.NET)
- sdk-spring-data-annotations.md — Annotate entities for Spring Data Cosmos
- sdk-spring-data-repository.md — Use CosmosRepository correctly
5. Indexing Strategies — MEDIUM-HIGH (prefix: index-)
- index-exclude-unused.md — Exclude paths never queried
- index-composite.md — Use composite indexes for ORDER BY
- index-spatial.md — Add spatial indexes for geo queries
- index-range-vs-hash.md — Choose appropriate index types
- index-lazy-consistent.md — Understand indexing modes
6. Throughput & Scaling — MEDIUM (prefix: throughput-)
- throughput-autoscale.md — Use autoscale for variable workloads
- throughput-right-size.md — Right-size provisioned throughput
- throughput-serverless.md — Consider serverless for dev/test
- throughput-burst.md — Understand burst capacity
- throughput-container-vs-database.md — Choose allocation level wisely
7. Global Distribution — MEDIUM (prefix: global-)
- global-multi-region.md — Configure multi-region writes
- global-consistency.md — Choose appropriate consistency level
- global-conflict-resolution.md — Implement conflict resolution
- global-failover.md — Configure automatic failover
- global-read-regions.md — Add read regions near users
- global-zone-redundancy.md — Enable zone redundancy for HA
8. Monitoring & Diagnostics — LOW-MEDIUM (prefix: monitoring-)
- monitoring-ru-consumption.md — Track RU consumption
- monitoring-latency.md — Monitor P99 latency
- monitoring-throttling.md — Alert on throttling
- monitoring-azure-monitor.md — Integrate Azure Monitor
- monitoring-diagnostic-logs.md — Enable diagnostic logging
9. Design Patterns — HIGH (prefix: pattern-)
- pattern-change-feed-materialized-views.md — Use Change Feed for materialized views
- pattern-efficient-ranking.md — Use count-based or cached approaches for ranking
- pattern-service-layer-relationships.md — Use a service layer to hydrate document references
10. Vector Search — HIGH (prefix: vector-)
- vector-enable-feature.md — Enable vector search feature on account
- vector-embedding-policy.md — Define vector embedding policy on container
- vector-index-type.md — Configure vector indexes (QuantizedFlat or DiskANN)
- vector-distance-query.md — Use VectorDistance() for similarity search
- vector-normalize-embeddings.md — Normalize embeddings for cosine similarity
- vector-repository-pattern.md — Implement repository pattern for vector search
How to Use
Each rule is a separate file under rules/. When answering a Cosmos DB question, read only the relevant rule file(s) based on the prefix matching the topic:
- Data modeling question → read
rules/model-*.mdfiles - Partition key question → read
rules/partition-*.mdfiles - SDK/client question → read
rules/sdk-*.mdfiles - etc.
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Additional context and references
Source: AzureCosmosDB/cosmosdb-agent-kit
1. Data Modeling (model)
Impact: CRITICAL Description: Proper data modeling is foundational to Cosmos DB performance. Poor modeling leads to expensive queries, excessive RU consumption, and scalability issues that are difficult to fix later.
2. Partition Key Design (partition)
Impact: CRITICAL Description: Partition key choice determines data distribution, query efficiency, and scalability limits. A bad partition key creates hot partitions and cross-partition query overhead.
3. Query Optimization (query)
Impact: HIGH Description: Optimized queries minimize RU consumption and latency. Inefficient queries cause unnecessary cross-partition scans and index misses.
4. SDK Best Practices (sdk)
Impact: HIGH Description: Proper SDK usage ensures connection efficiency, retry handling, and optimal throughput. Common mistakes include creating multiple clients and ignoring throttling.
5. Indexing Strategies (index)
Impact: MEDIUM-HIGH Description: Strategic indexing reduces query costs while minimizing write overhead. Default indexing often includes unused paths.
6. Throughput & Scaling (throughput)
Impact: MEDIUM Description: Right-sizing throughput balances cost and performance. Over-provisioning wastes money; under-provisioning causes throttling.
7. Global Distribution (global)
Impact: MEDIUM Description: Multi-region configuration enables low-latency reads globally and disaster recovery. Consistency choices impact both.
8. Monitoring & Diagnostics (monitoring)
Impact: LOW-MEDIUM Description: Proactive monitoring catches issues before they impact users. Diagnostics enable root cause analysis.
9. Design Patterns (pattern)
Impact: HIGH Description: Architecture patterns for common scenarios like cross-partition query optimization, event sourcing, and multi-tenant designs.
10. Vector Search (vector)
Impact: HIGH Description: Vector search configuration enables AI-powered semantic search and RAG patterns. Proper embedding storage, indexing, and query optimization are essential for performance and accuracy.
Rule Title Here
Impact: MEDIUM (optional impact description)
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications for Azure Cosmos DB.
Incorrect (description of what's wrong):
// Bad code example here
var container = cosmosClient.GetContainer("db", "container");
// Example of anti-patternCorrect (description of what's right):
// Good code example here
var container = cosmosClient.GetContainer("db", "container");
// Example of best practiceReference: Link to documentation or resource
Implement Conflict Resolution
Configure appropriate conflict resolution policies for multi-region write scenarios. Without proper handling, data can be lost.
Understanding conflicts:
// Conflicts occur when same document is written in multiple regions
// before replication completes
// Region A: Update order status to "shipped"
// Region B: Update order status to "cancelled" (same time)
// Both writes succeed locally, then conflict during replicationIncorrect (ignoring conflicts):
// Using default LWW with _ts but not understanding implications
// Later timestamp wins - but "later" may be wrong server
// Server A clock: 10:00:00.100 → "shipped"
// Server B clock: 10:00:00.050 → "cancelled"
// Result: "shipped" wins even though B's write may be logically laterCorrect (explicit conflict resolution):
// Option 1: Last Writer Wins with logical clock (recommended)
var containerProperties = new ContainerProperties
{
Id = "orders",
PartitionKeyPath = "/customerId",
ConflictResolutionPolicy = new ConflictResolutionPolicy
{
Mode = ConflictResolutionMode.LastWriterWins,
ResolutionPath = "/version" // Use application-managed version
}
};
// Document with version counter
public class Order
{
public string Id { get; set; }
public string CustomerId { get; set; }
public string Status { get; set; }
public long Version { get; set; } // Increment on each update
}
// Update with version increment
public async Task UpdateOrderStatus(Order order, string newStatus)
{
order.Status = newStatus;
order.Version++; // Higher version always wins
await container.UpsertItemAsync(order, new PartitionKey(order.CustomerId));
}// Option 2: Stored procedure for custom resolution
var containerWithCustom = new ContainerProperties
{
Id = "inventory",
PartitionKeyPath = "/productId",
ConflictResolutionPolicy = new ConflictResolutionPolicy
{
Mode = ConflictResolutionMode.Custom,
ResolutionProcedure = "dbs/mydb/colls/inventory/sprocs/resolveConflict"
}
};
// Stored procedure for custom logic
// Example: For inventory, take the LOWER value (conservative)
const string resolveConflictSproc = @"
function resolveConflict(incomingItem, existingItem, isTombstone, conflictingItems) {
if (isTombstone) {
// Delete wins
return existingItem;
}
// For inventory: lower quantity wins (conservative)
if (existingItem.quantity < incomingItem.quantity) {
return existingItem;
}
return incomingItem;
}";// Option 3: Read and resolve conflicts manually (async)
// Conflicts written to conflicts feed when no automatic resolution
var conflictsFeed = container.Conflicts.GetConflictQueryIterator<dynamic>();
while (conflictsFeed.HasMoreResults)
{
var conflicts = await conflictsFeed.ReadNextAsync();
foreach (var conflict in conflicts)
{
// Read conflicting versions
var conflictContent = await container.Conflicts.ReadCurrentAsync<Order>(
conflict, new PartitionKey(conflict.PartitionKey));
// Apply custom resolution logic
var resolvedOrder = ResolveOrderConflict(conflictContent.Resource);
// Write resolved version
await container.UpsertItemAsync(resolvedOrder);
// Delete conflict record
await container.Conflicts.DeleteAsync(conflict, new PartitionKey(conflict.PartitionKey));
}
}Best practices:
- Use LWW with application-controlled version for simple cases
- Use stored procedures when business logic determines winner
- Monitor conflicts feed if using Custom mode
- Design to minimize conflicts (partition by user, idempotent operations)
Reference: Conflict resolution
Choose Appropriate Consistency Level
Select the consistency level that matches your application's requirements. Each level has different tradeoffs for latency, availability, and consistency.
Consistency levels (strongest to weakest):
// STRONG - Linearizable reads
// Reads always see most recent committed write
// Highest latency, lowest availability in multi-region
var client = new CosmosClient(connectionString, new CosmosClientOptions
{
ConsistencyLevel = ConsistencyLevel.Strong
});
// Use: Financial transactions, inventory management
// Tradeoff: Higher latency, reduced availability during regional outage
// BOUNDED STALENESS - Reads lag behind writes by bounded amount
// "Reads at least this fresh" guarantee
var client = new CosmosClient(connectionString, new CosmosClientOptions
{
ConsistencyLevel = ConsistencyLevel.BoundedStaleness
});
// Use: Stock tickers, leaderboards (where slight delay is OK)
// Tradeoff: May read slightly old data, better performance than Strong
// SESSION (DEFAULT) - Monotonic reads within session
// Client always sees its own writes
var client = new CosmosClient(connectionString, new CosmosClientOptions
{
ConsistencyLevel = ConsistencyLevel.Session
});
// Use: Most applications - user sees their changes
// Best balance of consistency and performance
// CONSISTENT PREFIX - Reads never see out-of-order writes
// Guarantees ordering but may lag behind
var client = new CosmosClient(connectionString, new CosmosClientOptions
{
ConsistencyLevel = ConsistencyLevel.ConsistentPrefix
});
// Use: Event sourcing, activity feeds
// Tradeoff: May read stale data, but always in order
// EVENTUAL - Weakest, highest performance
// No ordering guarantees, eventually converges
var client = new CosmosClient(connectionString, new CosmosClientOptions
{
ConsistencyLevel = ConsistencyLevel.Eventual
});
// Use: View counts, likes, non-critical telemetry
// Best performance, lowest costCorrect (choosing based on requirements):
// Example: E-commerce platform
// Orders container - Strong or Session
// User must see their order immediately after placing
var ordersClient = new CosmosClient(connectionString, new CosmosClientOptions
{
ConsistencyLevel = ConsistencyLevel.Session // Recommended
});
// Product catalog - Eventual or Consistent Prefix
// Slight delay in inventory updates is acceptable
var catalogClient = new CosmosClient(connectionString, new CosmosClientOptions
{
ConsistencyLevel = ConsistencyLevel.Eventual
});
// Analytics/metrics - Eventual
// Historical data doesn't need immediate consistency
var analyticsClient = new CosmosClient(connectionString, new CosmosClientOptions
{
ConsistencyLevel = ConsistencyLevel.Eventual
});// Session consistency with session token (most common pattern)
// SDK handles session tokens automatically within a client instance
// For scenarios where you need to share session across requests:
var response = await container.CreateItemAsync(order);
var sessionToken = response.Headers["x-ms-session-token"];
// Later request can use same session for read-your-writes
var readOptions = new ItemRequestOptions
{
SessionToken = sessionToken
};
var order = await container.ReadItemAsync<Order>(id, pk, readOptions);RU cost comparison (relative to Strong):
- Strong: 2x RU for reads (waits for quorum)
- Bounded Staleness: 2x RU for reads
- Session: 1x RU (default)
- Consistent Prefix: 1x RU
- Eventual: 1x RU
Reference: Consistency levels
Configure Automatic Failover
Enable automatic failover for high availability. Without it, regional outages require manual intervention.
Incorrect (no failover configuration):
// Multi-region account without automatic failover
// If primary region goes down:
// - Manual intervention required
// - Downtime until you notice and trigger failover
// - MTTR (Mean Time To Recovery) = hours potentially
// ARM template without failover
{
"properties": {
"enableAutomaticFailover": false, // DEFAULT - dangerous!
"locations": [
{ "locationName": "West US 2", "failoverPriority": 0 },
{ "locationName": "East US 2", "failoverPriority": 1 }
]
}
}Correct (automatic failover enabled):
// ARM template with automatic failover
{
"type": "Microsoft.DocumentDB/databaseAccounts",
"apiVersion": "2021-10-15",
"name": "my-cosmos-account",
"properties": {
"enableAutomaticFailover": true, // Enable automatic failover!
// Define failover priority order
"locations": [
{
"locationName": "West US 2",
"failoverPriority": 0, // Primary
"isZoneRedundant": true // Zone redundancy for HA
},
{
"locationName": "East US 2",
"failoverPriority": 1 // First failover target
},
{
"locationName": "West Europe",
"failoverPriority": 2 // Second failover target
}
]
}
}// Configure SDK to handle failovers gracefully
var client = new CosmosClient(connectionString, new CosmosClientOptions
{
ApplicationName = "MyApp",
// SDK will automatically discover new endpoints after failover
EnableTcpConnectionEndpointRediscovery = true,
// Preferred regions in priority order
ApplicationPreferredRegions = new List<string>
{
Regions.WestUS2, // Primary
Regions.EastUS2, // Failover 1
Regions.WestEurope // Failover 2
},
// Connection will retry and discover new primary
MaxRetryAttemptsOnRateLimitedRequests = 9,
MaxRetryWaitTimeOnRateLimitedRequests = TimeSpan.FromSeconds(30)
});
// SDK handles failover transparently - your code doesn't change
await container.CreateItemAsync(order, new PartitionKey(order.CustomerId));
// If West US 2 is down, SDK automatically routes to East US 2// Monitor failover status
var accountProperties = await client.ReadAccountAsync();
Console.WriteLine($"Write regions: {string.Join(", ",
accountProperties.WritableRegions.Select(r => r.Name))}");
Console.WriteLine($"Read regions: {string.Join(", ",
accountProperties.ReadableRegions.Select(r => r.Name))}");
// Set up Azure Monitor alerts for:
// - Region failover events
// - Replication lag metrics
// - Availability metrics// Test failover (non-production)
// Azure CLI command to trigger manual failover
// az cosmosdb failover-priority-change \
// --name mycosmosdb \
// --resource-group myrg \
// --failover-policies "East US 2"=0 "West US 2"=1
// Monitor your application behavior during failover test
// Expect: brief increase in latency, no data lossAutomatic failover behavior:
- Triggered after region unresponsive for ~1 minute
- Promotes next region in priority order
- SDK automatically reconnects to new primary
- No data loss with synchronous replication
Reference: Automatic failover
Configure Multi-Region Writes
Enable multi-region writes for globally distributed applications. Allows writes to any region with automatic conflict resolution.
Incorrect (single write region):
// Default: Single write region
// All writes must travel to one region
// Users in Asia writing to US region: 200-300ms latency
// No multi-region write configuration
var client = new CosmosClient(connectionString);
// Write from Asia still goes to US (write region)
await container.CreateItemAsync(order); // 200ms+ latency for Asian usersCorrect (multi-region writes enabled):
// Step 1: Enable multi-region writes on account (Azure Portal or ARM)
{
"type": "Microsoft.DocumentDB/databaseAccounts",
"properties": {
"enableMultipleWriteLocations": true, // Enable multi-region writes
"locations": [
{ "locationName": "West US 2", "failoverPriority": 0 },
{ "locationName": "East Asia", "failoverPriority": 1 },
{ "locationName": "West Europe", "failoverPriority": 2 }
]
}
}
// Step 2: Configure SDK to write locally
var client = new CosmosClient(connectionString, new CosmosClientOptions
{
// SDK automatically routes to nearest region
ApplicationPreferredRegions = new List<string>
{
Regions.EastAsia, // First choice (if deployed in Asia)
Regions.WestUS2,
Regions.WestEurope
}
});
// Write goes to nearest region (East Asia for Asian users)
await container.CreateItemAsync(order); // <10ms latency locally!// Step 3: Handle conflicts (Last Writer Wins is default)
// For custom conflict resolution, configure container
// Last Writer Wins (LWW) - Default
// Uses _ts (timestamp) to determine winner
var containerWithLWW = new ContainerProperties
{
Id = "orders",
PartitionKeyPath = "/customerId",
ConflictResolutionPolicy = new ConflictResolutionPolicy
{
Mode = ConflictResolutionMode.LastWriterWins,
ResolutionPath = "/_ts" // Higher timestamp wins
}
};
// Custom resolution path (e.g., version number)
var containerWithCustomLWW = new ContainerProperties
{
Id = "products",
PartitionKeyPath = "/categoryId",
ConflictResolutionPolicy = new ConflictResolutionPolicy
{
Mode = ConflictResolutionMode.LastWriterWins,
ResolutionPath = "/version" // Higher version wins
}
};// Verify multi-region write is working
var accountProperties = await client.ReadAccountAsync();
Console.WriteLine($"Multi-region writes: {accountProperties.EnableMultipleWriteLocations}");
Console.WriteLine($"Write regions: {string.Join(", ",
accountProperties.WritableRegions.Select(r => r.Name))}");Benefits:
- Local write latency (< 10ms vs 200ms+)
- Higher write availability (any region can accept writes)
- Better disaster recovery
Considerations:
- Higher cost (replication in both directions)
- Requires conflict resolution strategy
- Some operations have restrictions (stored procedures)
Reference: Multi-region writes
Add Read Regions Near Users
Add read regions in geographic locations close to your users. Reads can be served from any region, reducing latency for global users.
Incorrect (single region for global users):
// Only one region configured
// Users from all locations read from single region
// Asia users → 200ms+ latency to US region
// Europe users → 100ms+ latency to US region
{
"properties": {
"locations": [
{ "locationName": "West US 2", "failoverPriority": 0 }
]
}
}Correct (read regions near user populations):
// Add read replicas near major user bases
{
"type": "Microsoft.DocumentDB/databaseAccounts",
"properties": {
"locations": [
// Primary write region
{
"locationName": "West US 2",
"failoverPriority": 0
},
// Read replica for European users
{
"locationName": "West Europe",
"failoverPriority": 1
},
// Read replica for Asian users
{
"locationName": "Southeast Asia",
"failoverPriority": 2
},
// Read replica for Australian users
{
"locationName": "Australia East",
"failoverPriority": 3
}
]
}
}// Configure SDK for region-local reads
// Deployed in Europe - prioritize European region
var europeClient = new CosmosClient(connectionString, new CosmosClientOptions
{
ApplicationPreferredRegions = new List<string>
{
Regions.WestEurope, // Nearest region first
Regions.NorthEurope, // Backup within Europe
Regions.WestUS2 // Primary (for writes)
}
});
// Deployed in Asia - prioritize Asian region
var asiaClient = new CosmosClient(connectionString, new CosmosClientOptions
{
ApplicationPreferredRegions = new List<string>
{
Regions.SoutheastAsia, // Nearest region first
Regions.EastAsia, // Backup within Asia
Regions.WestUS2 // Primary (for writes)
}
});// Dynamic region selection based on deployment
public static CosmosClient CreateRegionalClient(string connectionString)
{
var deploymentRegion = Environment.GetEnvironmentVariable("AZURE_REGION")
?? "westus2";
var preferredRegions = deploymentRegion.ToLower() switch
{
"westeurope" or "northeurope" => new List<string>
{
Regions.WestEurope, Regions.NorthEurope, Regions.WestUS2
},
"southeastasia" or "eastasia" => new List<string>
{
Regions.SoutheastAsia, Regions.EastAsia, Regions.WestUS2
},
"australiaeast" => new List<string>
{
Regions.AustraliaEast, Regions.SoutheastAsia, Regions.WestUS2
},
_ => new List<string>
{
Regions.WestUS2, Regions.EastUS2
}
};
return new CosmosClient(connectionString, new CosmosClientOptions
{
ApplicationPreferredRegions = preferredRegions
});
}// Verify reads are going to correct region
var response = await container.ReadItemAsync<Order>(orderId, pk);
// Check diagnostics for contacted region
var diagnostics = response.Diagnostics.ToString();
_logger.LogDebug("Request served from: {Diagnostics}", diagnostics);
// Look for "Contacted Region" in diagnosticsCost considerations:
- Each read replica adds cost (~same as primary)
- Calculate: User latency improvement × request volume vs. replica cost
- Start with regions serving most users, add more based on metrics
Reference: Global distribution
Configure Zone Redundancy for High Availability
Enable zone redundancy to protect against availability zone failures. Zone-redundant accounts distribute replicas across multiple availability zones within a region.
Incorrect (no zone redundancy):
// Single-region account without zone redundancy
// If an availability zone fails:
// - Potential data loss
// - Availability loss until recovery
// - SLA: 99.99%
{
"type": "Microsoft.DocumentDB/databaseAccounts",
"properties": {
"locations": [
{
"locationName": "East US",
"failoverPriority": 0,
"isZoneRedundant": false // DEFAULT - no zone protection!
}
]
}
}Correct (zone redundancy enabled):
// ARM template with zone redundancy
{
"type": "Microsoft.DocumentDB/databaseAccounts",
"apiVersion": "2023-04-15",
"name": "my-cosmos-account",
"properties": {
"locations": [
{
"locationName": "East US",
"failoverPriority": 0,
"isZoneRedundant": true // Enable zone redundancy!
},
{
"locationName": "West US",
"failoverPriority": 1,
"isZoneRedundant": true // Enable in secondary too
}
]
}
}// Bicep template with zone redundancy
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = {
name: 'my-cosmos-account'
location: 'East US'
properties: {
locations: [
{
locationName: 'East US'
failoverPriority: 0
isZoneRedundant: true // Replicas spread across 3 AZs
}
{
locationName: 'West US'
failoverPriority: 1
isZoneRedundant: true
}
]
enableAutomaticFailover: true
}
}SLA Improvements with Zone Redundancy:
| Configuration | Write SLA | Read SLA | Zone Failure | Regional Failure |
|---|---|---|---|---|
| Single region, no ZR | 99.99% | 99.99% | Data/availability loss | Data/availability loss |
| Single region + ZR | 99.995% | 99.995% | No loss | Data/availability loss |
| Multi-region, no ZR | 99.99% | 99.999% | Data/availability loss | Dependent on consistency |
| Multi-region + ZR | 99.995% | 99.999% | No loss | Dependent on consistency |
| Multi-region writes + ZR | 99.999% | 99.999% | No loss | No loss (with conflicts) |
Cost Considerations:
- Zone redundancy adds 25% premium to provisioned throughput
- Premium is waived for:
- Multi-region write accounts
- Autoscale collections
- Adding a region adds ~100% to existing bill
When to Enable Zone Redundancy:
1. Always for single-region accounts - Primary protection against AZ failures 2. Write regions in multi-region accounts - Protects write availability 3. Production workloads - Required for high SLA guarantees
Regions Supporting Zone Redundancy:
Check current availability: Azure regions with availability zones
Reference: High availability in Azure Cosmos DB
Use Composite Indexes for ORDER BY
Create composite indexes for queries with ORDER BY on multiple properties. Without them, queries may fail or require expensive client-side sorting.
Incorrect (ORDER BY without composite index):
// Query with multi-property ORDER BY
var query = @"
SELECT * FROM c
WHERE c.status = 'active'
ORDER BY c.createdAt DESC, c.priority ASC";
// Without composite index, this may:
// 1. Fail with: "Order-by item requires a corresponding composite index"
// 2. Or consume excessive RU for sortingCorrect (composite index for ORDER BY):
// Create composite index matching the ORDER BY
var indexingPolicy = new IndexingPolicy
{
IndexingMode = IndexingMode.Consistent,
CompositeIndexes =
{
// Must match ORDER BY exactly (properties and sort order)
new Collection<CompositePath>
{
new CompositePath { Path = "/createdAt", Order = CompositePathSortOrder.Descending },
new CompositePath { Path = "/priority", Order = CompositePathSortOrder.Ascending }
},
// Add reverse order for flexibility
new Collection<CompositePath>
{
new CompositePath { Path = "/createdAt", Order = CompositePathSortOrder.Ascending },
new CompositePath { Path = "/priority", Order = CompositePathSortOrder.Descending }
},
// Common filter + sort pattern
new Collection<CompositePath>
{
new CompositePath { Path = "/status", Order = CompositePathSortOrder.Ascending },
new CompositePath { Path = "/createdAt", Order = CompositePathSortOrder.Descending }
}
}
};
var containerProperties = new ContainerProperties
{
Id = "tasks",
PartitionKeyPath = "/userId",
IndexingPolicy = indexingPolicy
};// JSON indexing policy with composite indexes
{
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [
{ "path": "/*" }
],
"compositeIndexes": [
[
{ "path": "/status", "order": "ascending" },
{ "path": "/createdAt", "order": "descending" }
],
[
{ "path": "/createdAt", "order": "descending" },
{ "path": "/priority", "order": "ascending" }
]
]
}// Common patterns that need composite indexes:
// Pattern 1: Filter + Sort
// WHERE status = 'x' ORDER BY date DESC
new Collection<CompositePath>
{
new CompositePath { Path = "/status", Order = CompositePathSortOrder.Ascending },
new CompositePath { Path = "/date", Order = CompositePathSortOrder.Descending }
}
// Pattern 2: Multi-column sort
// ORDER BY lastName ASC, firstName ASC
new Collection<CompositePath>
{
new CompositePath { Path = "/lastName", Order = CompositePathSortOrder.Ascending },
new CompositePath { Path = "/firstName", Order = CompositePathSortOrder.Ascending }
}
// Pattern 3: Range + Sort
// WHERE price >= 10 ORDER BY rating DESC
new Collection<CompositePath>
{
new CompositePath { Path = "/price", Order = CompositePathSortOrder.Ascending },
new CompositePath { Path = "/rating", Order = CompositePathSortOrder.Descending }
}Multi-Tenant Composite Index Patterns
In multi-tenant designs using type discriminators and hierarchical partition keys, composite indexes are critical for queries that filter by entity type and sort by common fields:
// Multi-tenant SaaS: tasks by status, sorted by date
{
"compositeIndexes": [
[
{ "path": "/type", "order": "ascending" },
{ "path": "/status", "order": "ascending" },
{ "path": "/createdAt", "order": "descending" }
],
[
{ "path": "/type", "order": "ascending" },
{ "path": "/assigneeId", "order": "ascending" },
{ "path": "/dueDate", "order": "ascending" }
],
[
{ "path": "/type", "order": "ascending" },
{ "path": "/priority", "order": "descending" },
{ "path": "/createdAt", "order": "descending" }
]
]
}// Java: Composite indexes with IndexingPolicy
IndexingPolicy policy = new IndexingPolicy();
// Type + Status + Date (for: WHERE type='task' AND status='open' ORDER BY createdAt DESC)
List<CompositePath> statusSort = Arrays.asList(
new CompositePath().setPath("/type").setOrder(CompositePathSortOrder.ASCENDING),
new CompositePath().setPath("/status").setOrder(CompositePathSortOrder.ASCENDING),
new CompositePath().setPath("/createdAt").setOrder(CompositePathSortOrder.DESCENDING)
);
// Type + Assignee + DueDate (for: WHERE type='task' AND assigneeId=@id ORDER BY dueDate)
List<CompositePath> assigneeSort = Arrays.asList(
new CompositePath().setPath("/type").setOrder(CompositePathSortOrder.ASCENDING),
new CompositePath().setPath("/assigneeId").setOrder(CompositePathSortOrder.ASCENDING),
new CompositePath().setPath("/dueDate").setOrder(CompositePathSortOrder.ASCENDING)
);
policy.setCompositeIndexes(Arrays.asList(statusSort, assigneeSort));Why type discriminators need composite indexes: When a single container holds multiple entity types (tenant, user, project, task), queries always filter by type. Without a composite index on (type, sortField), the query engine cannot efficiently sort within a single entity type. This is especially costly in containers with millions of mixed-type documents.
Rules:
- Composite index order must match ORDER BY exactly
- First path can be equality filter
- Include both ASC/DESC variants for flexibility
- Maximum 8 paths per composite index
- Always define composite indexes when using type discriminators in shared containers
- Include
/typeas the first path in multi-tenant composite indexes
Reference: Composite indexes
Exclude Unused Index Paths
Exclude paths from indexing that you never query. Every indexed path adds write cost with no read benefit.
Incorrect (indexing everything):
// Default indexing policy indexes ALL paths
// Great for flexibility, expensive for writes
{
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [
{
"path": "/*" // Indexes everything including unused fields
}
],
"excludedPaths": []
}
// Document with large unused fields gets indexed unnecessarily
{
"id": "order-123",
"customerId": "cust-1", // Queried
"status": "shipped", // Queried
"items": [...], // Not queried
"internalNotes": "...", // Not queried
"auditLog": [...] // Large array, never queried!
}
// Write cost includes indexing auditLog array - wasted RUCorrect (selective indexing):
// Include only queried paths
var indexingPolicy = new IndexingPolicy
{
IndexingMode = IndexingMode.Consistent,
Automatic = true,
// Only include paths you actually query
IncludedPaths =
{
new IncludedPath { Path = "/customerId/?" },
new IncludedPath { Path = "/status/?" },
new IncludedPath { Path = "/orderDate/?" },
new IncludedPath { Path = "/total/?" }
},
// Exclude everything else (especially large arrays)
ExcludedPaths =
{
new ExcludedPath { Path = "/items/*" }, // Embedded array
new ExcludedPath { Path = "/internalNotes/?" },
new ExcludedPath { Path = "/auditLog/*" }, // Large array
new ExcludedPath { Path = "/_etag/?" } // System field
}
};
var containerProperties = new ContainerProperties
{
Id = "orders",
PartitionKeyPath = "/customerId",
IndexingPolicy = indexingPolicy
};// JSON equivalent indexing policy
{
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [
{ "path": "/customerId/?" },
{ "path": "/status/?" },
{ "path": "/orderDate/?" }
],
"excludedPaths": [
{ "path": "/items/*" },
{ "path": "/auditLog/*" },
{ "path": "/*" } // Exclude all other paths
]
}// Alternative: exclude all, include specific
var indexingPolicy = new IndexingPolicy
{
IndexingMode = IndexingMode.Consistent,
// Start with exclude all
ExcludedPaths = { new ExcludedPath { Path = "/*" } },
// Explicitly include only what you query
IncludedPaths =
{
new IncludedPath { Path = "/customerId/?" },
new IncludedPath { Path = "/orderDate/?" }
}
};Monitor and adjust:
- Review query patterns periodically
- Use Query Stats to see index utilization
- Balance write cost reduction vs query flexibility
Reference: Indexing policies
Understand Indexing Modes
Choose the appropriate indexing mode based on your workload. Consistent mode ensures query results are current; None disables indexing entirely.
Indexing modes explained:
// CONSISTENT MODE (Default - recommended for most cases)
// Indexes are updated synchronously with writes
// Queries always see latest data
var consistentPolicy = new IndexingPolicy
{
IndexingMode = IndexingMode.Consistent, // Default
Automatic = true
};
// Benefits:
// - Query results are always up-to-date
// - Strong consistency between writes and reads
// Tradeoffs:
// - Write latency includes index update time// NONE MODE (Write-only containers)
// No automatic indexing - fastest writes
// Only point reads work (by id + partition key)
var nonePolicy = new IndexingPolicy
{
IndexingMode = IndexingMode.None,
Automatic = false
};
// Use cases:
// - Pure key-value store (only point reads)
// - High-volume write ingestion
// - Time-series data queried via external system (Synapse Link)Correct (choosing mode based on workload):
// Typical transactional workload - use Consistent
var ordersPolicy = new IndexingPolicy
{
IndexingMode = IndexingMode.Consistent,
Automatic = true,
IncludedPaths = { new IncludedPath { Path = "/*" } }
};
var ordersContainer = new ContainerProperties
{
Id = "orders",
PartitionKeyPath = "/customerId",
IndexingPolicy = ordersPolicy
};
// Queries immediately see new orders// High-volume telemetry ingestion - consider None
var telemetryPolicy = new IndexingPolicy
{
IndexingMode = IndexingMode.None, // Maximum write throughput
Automatic = false
};
var telemetryContainer = new ContainerProperties
{
Id = "telemetry",
PartitionKeyPath = "/deviceId",
IndexingPolicy = telemetryPolicy,
// Enable analytical store for querying via Synapse
AnalyticalStorageTimeToLiveInSeconds = -1
};
// Point reads still work
var reading = await container.ReadItemAsync<Telemetry>(
readingId, new PartitionKey(deviceId));
// Complex queries via Synapse Link (analytical store)
// No indexing overhead on transactional writes// Selective indexing - best of both worlds
var hybridPolicy = new IndexingPolicy
{
IndexingMode = IndexingMode.Consistent,
Automatic = true,
// Only index fields you query
IncludedPaths =
{
new IncludedPath { Path = "/customerId/?" },
new IncludedPath { Path = "/orderDate/?" }
},
ExcludedPaths =
{
new ExcludedPath { Path = "/*" } // Exclude everything else
}
};
// Fast writes (minimal indexing) + efficient queries (on indexed paths)Decision guide:
- Consistent: Default, transactional workloads, need queries
- None: Write-only, pure key-value, using Synapse Link for analytics
Note: Lazy mode was deprecated - use Consistent instead.
Reference: Indexing modes
Choose Appropriate Index Types
Understand when to use different index types. Range indexes support equality, range, and ORDER BY; Hash indexes are deprecated.
Understanding index types:
// Range Index (DEFAULT - recommended for most cases)
// Supports: =, >, <, >=, <=, !=, ORDER BY, JOINs
// Index entries: ["a"], ["a", "b"], ["a", "b", "c"]...
{
"includedPaths": [
{
"path": "/price/?",
"indexes": [
{
"kind": "Range", // Default, most flexible
"dataType": "Number",
"precision": -1 // -1 = maximum precision
},
{
"kind": "Range",
"dataType": "String",
"precision": -1
}
]
}
]
}Correct (modern indexing approach):
// Modern Cosmos DB automatically uses optimal index types
// You typically just specify paths, not index kinds
var indexingPolicy = new IndexingPolicy
{
IndexingMode = IndexingMode.Consistent,
Automatic = true,
// Just specify paths - Cosmos DB handles index types
IncludedPaths =
{
new IncludedPath { Path = "/category/?" }, // Equality queries
new IncludedPath { Path = "/price/?" }, // Range queries
new IncludedPath { Path = "/createdAt/?" }, // ORDER BY
new IncludedPath { Path = "/tags/*" } // Array elements
},
ExcludedPaths =
{
new ExcludedPath { Path = "/description/?" }, // Large text, not queried
new ExcludedPath { Path = "/metadata/*" } // Nested object, not queried
}
};// For special query patterns, add composite or spatial indexes
var indexingPolicy = new IndexingPolicy
{
// Standard range indexes (automatic)
IncludedPaths =
{
new IncludedPath { Path = "/*" } // Index everything by default
},
// Composite indexes for multi-property ORDER BY
CompositeIndexes =
{
new Collection<CompositePath>
{
new CompositePath { Path = "/category", Order = CompositePathSortOrder.Ascending },
new CompositePath { Path = "/price", Order = CompositePathSortOrder.Descending }
}
},
// Spatial indexes for geo queries
SpatialIndexes =
{
new SpatialPath
{
Path = "/location/?",
SpatialTypes = { SpatialType.Point }
}
}
};// JSON policy showing all index types
{
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [
{ "path": "/*" }
],
"excludedPaths": [
{ "path": "/largeContent/?" }
],
"compositeIndexes": [
[
{ "path": "/status", "order": "ascending" },
{ "path": "/createdAt", "order": "descending" }
]
],
"spatialIndexes": [
{
"path": "/location/?",
"types": ["Point"]
}
]
}Index type summary:
- Range (default): Equality, range, ORDER BY - use for everything
- Composite: Multi-property ORDER BY, filter+sort
- Spatial: Geographic/geometric queries
- Hash: DEPRECATED - don't use
Reference: Index types
Add Spatial Indexes for Geo Queries
Create spatial indexes for properties that store geographic data when you need to perform proximity or geometry queries.
Incorrect (geo queries without spatial index):
// Document with location
{
"id": "store-1",
"name": "Downtown Store",
"location": {
"type": "Point",
"coordinates": [-122.4194, 37.7749] // [longitude, latitude]
}
}
// Query without spatial index - expensive full scan!
var query = @"
SELECT * FROM c
WHERE ST_DISTANCE(c.location, {'type':'Point','coordinates':[-122.4,37.7]}) < 5000";Correct (spatial index for location queries):
// Create indexing policy with spatial index
var indexingPolicy = new IndexingPolicy
{
IndexingMode = IndexingMode.Consistent,
// Include path with spatial index
SpatialIndexes =
{
new SpatialPath
{
Path = "/location/?",
SpatialTypes =
{
SpatialType.Point
}
}
}
};
// If you have multiple geometry types
var indexingPolicyMulti = new IndexingPolicy
{
SpatialIndexes =
{
// Store locations as points
new SpatialPath
{
Path = "/location/?",
SpatialTypes = { SpatialType.Point }
},
// Delivery zones as polygons
new SpatialPath
{
Path = "/deliveryArea/?",
SpatialTypes = { SpatialType.Polygon }
}
}
};// JSON indexing policy with spatial index
{
"indexingMode": "consistent",
"spatialIndexes": [
{
"path": "/location/?",
"types": ["Point"]
},
{
"path": "/boundaries/?",
"types": ["Polygon"]
}
]
}// Efficient spatial queries with index
// Find stores within 5km of user
var nearbyQuery = @"
SELECT c.name, c.address,
ST_DISTANCE(c.location, @userLocation) AS distanceMeters
FROM c
WHERE ST_DISTANCE(c.location, @userLocation) < 5000
ORDER BY ST_DISTANCE(c.location, @userLocation)";
var userLocation = new
{
type = "Point",
coordinates = new[] { -122.4194, 37.7749 }
};
var stores = await container.GetItemQueryIterator<Store>(
new QueryDefinition(nearbyQuery)
.WithParameter("@userLocation", userLocation)
).ReadNextAsync();
// Check if point is within polygon (delivery zone)
var withinQuery = @"
SELECT * FROM c
WHERE ST_WITHIN(@orderLocation, c.deliveryArea)";
// Find intersecting regions
var intersectQuery = @"
SELECT * FROM c
WHERE ST_INTERSECTS(c.boundaries, @searchArea)";Supported spatial functions:
ST_DISTANCE- Distance between geometriesST_WITHIN- Point within polygonST_INTERSECTS- Geometries intersectST_ISVALID- Validate GeoJSONST_ISVALIDDETAILED- Validation with details
Reference: Geospatial queries
Keep Items Well Under 2MB Limit
Azure Cosmos DB enforces a 2MB maximum item size. Design documents to stay well under this limit to avoid runtime failures.
Incorrect (risk of hitting limit):
// Anti-pattern: storing large binary data in documents
public class Document
{
public string Id { get; set; }
public string Name { get; set; }
// Large base64-encoded file content - DANGER!
public string FileContent { get; set; } // Could be megabytes
// Or large arrays that grow
public List<AuditEntry> AuditLog { get; set; } // Unbounded
}
// This will fail when content exceeds 2MB
await container.CreateItemAsync(doc);
// Microsoft.Azure.Cosmos.CosmosException: Request Entity Too LargeCorrect (bounded document size):
// Store metadata in Cosmos DB, large content in Blob Storage
public class Document
{
public string Id { get; set; }
public string Name { get; set; }
public long FileSizeBytes { get; set; }
public string ContentType { get; set; }
// Reference to blob storage instead of inline content
public string BlobUri { get; set; }
// Keep only recent/relevant audit entries
public List<AuditEntry> RecentAuditEntries { get; set; } // Max 10-20 items
}
// Large content goes to Blob Storage
await blobClient.UploadAsync(largeFileStream);
var doc = new Document
{
Id = Guid.NewGuid().ToString(),
Name = "large-file.pdf",
BlobUri = blobClient.Uri.ToString()
};
await container.CreateItemAsync(doc);Size monitoring:
// Check item size before writing
var json = JsonSerializer.Serialize(item);
var sizeBytes = Encoding.UTF8.GetByteCount(json);
if (sizeBytes > 1_500_000) // 1.5MB warning threshold
{
_logger.LogWarning("Item approaching size limit: {SizeKB}KB", sizeBytes / 1024);
}Reference: Azure Cosmos DB service quotas
Denormalize for Read-Heavy Workloads
In read-heavy workloads, denormalize frequently-queried data to avoid expensive lookups. Accept write overhead for faster reads.
Incorrect (normalized requires multiple queries):
// Displaying product list with category names
public class Product
{
public string Id { get; set; }
public string Name { get; set; }
public string CategoryId { get; set; } // Just the ID
public decimal Price { get; set; }
}
// To display "Product Name - Category Name" requires JOIN-like pattern:
var products = await GetProductsAsync();
foreach (var product in products)
{
// N+1 query problem!
var category = await container.ReadItemAsync<Category>(
product.CategoryId, new PartitionKey(product.CategoryId));
product.CategoryName = category.Name;
}
// 1 + N queries = terrible performanceCorrect (denormalized for read efficiency):
public class Product
{
public string Id { get; set; }
public string Name { get; set; }
public string CategoryId { get; set; }
// Denormalized category info for display
public string CategoryName { get; set; }
public string CategorySlug { get; set; }
public decimal Price { get; set; }
}
// Single query returns everything needed for display
var query = "SELECT c.id, c.name, c.categoryName, c.price FROM c WHERE c.type = 'product'";
var products = await container.GetItemQueryIterator<Product>(query).ReadNextAsync();
// No additional queries needed!
// When category changes, update products using Change Feed
public async Task HandleCategoryChange(Category category)
{
var query = $"SELECT * FROM c WHERE c.categoryId = '{category.Id}'";
await foreach (var product in container.GetItemQueryIterator<Product>(query))
{
product.CategoryName = category.Name;
await container.UpsertItemAsync(product);
}
}Denormalize when:
- Read-to-write ratio is high (10:1 or more)
- Denormalized data changes infrequently
- Query patterns benefit from co-located data
Additional strategies to consider for denormalization: Short-Circuit Denormalization :
- Definition: Duplicate only specific fields (not the full related document) to avoid a cross-partition lookup
- When to use:
- The duplicated property is mostly immutable (e.g., product name) or the app can tolerate staleness
- The property is small (a string, not an object)
- The access pattern would otherwise require a cross-partition read
- Example: Copy
customerNameinto Order doc to avoid looking up the Customer doc
Workload-Driven Cost Comparison Template for Denormalization Strategy :
Option 1 — Denormalized:
Read cost: [read_RPS] × [RU_per_read] = X RU/s
Write cost: [write_RPS] × [RU_per_write] + [update_propagation_cost] = Y RU/s
Total: X + Y RU/s
Option 2 — Normalized:
Read cost: [read_RPS] × ([RU_per_read] + [RU_for_lookup]) = X' RU/s
Write cost: [write_RPS] × [RU_per_write] = Y' RU/s
Total: X' + Y' RU/s
Decision: Choose option with lower total RU/s when workload profile details availableReference: Denormalization patterns
Embed Related Data Retrieved Together
Embed related data within a single document when they're always accessed together. This eliminates the need for multiple queries (Cosmos DB has no JOINs across documents).
Incorrect (requires multiple queries):
// Separate documents require multiple round-trips
var order = await container.ReadItemAsync<Order>(orderId, new PartitionKey(customerId));
var customer = await container.ReadItemAsync<Customer>(order.CustomerId, new PartitionKey(order.CustomerId));
var items = await container.GetItemQueryIterator<OrderItem>(
$"SELECT * FROM c WHERE c.orderId = '{orderId}'").ReadNextAsync();
// 3 separate queries = 3x latency + 3x RU costCorrect (single read operation):
// Embedded document - single query retrieves everything
public class Order
{
public string Id { get; set; }
public string CustomerId { get; set; }
// Embedded customer summary (not full customer document)
public CustomerSummary Customer { get; set; }
// Embedded order items
public List<OrderItem> Items { get; set; }
public decimal Total { get; set; }
public DateTime OrderDate { get; set; }
}
// Single read gets everything needed
var order = await container.ReadItemAsync<Order>(orderId, new PartitionKey(customerId));
// 1 query = lowest latency + minimal RUEmbed when:
- Data is read together frequently
- Embedded data changes infrequently
- Embedded data is bounded in size
Consider following Aggregate Decision Framework for embedding vs referencing: 1. Access Correlation Thresholds
- \>90% accessed together → Strong single-document aggregate candidate (embed)
- 50–90% accessed together → Multi-document container aggregate candidate (same container, separate docs, shared partition key)
- <50% accessed together → Separate containers
2. Constraint Checks :
- Size: Will combined size exceed 1MB? → Force multi-document or separate containers for child documents
- Updates: Different update frequencies? → Consider multi-document
- Atomicity: Need transactional updates? → Favor same partition with small batched updates or distributed transactional outbox pattern
Reference: Data modeling in Azure Cosmos DB
Follow ID Value Length and Character Constraints
Azure Cosmos DB enforces a 1,023 byte maximum for the id property and restricts certain characters. Using non-alphanumeric characters causes interoperability problems across SDKs, connectors, and tools.
Incorrect (oversized or problematic IDs):
// Anti-pattern 1: ID derived from unbounded user input
public class Document
{
// ID could exceed 1,023 bytes if title is very long
public string Id => $"{Category}_{SubCategory}_{Title}_{Description}";
public string Category { get; set; }
public string SubCategory { get; set; }
public string Title { get; set; }
public string Description { get; set; } // Unbounded!
}
// Anti-pattern 2: IDs containing forbidden or problematic characters
var doc = new Document
{
Id = "files/reports\\2026/Q1", // Contains '/' and '\' - FORBIDDEN
Content = "..."
};
await container.CreateItemAsync(doc);
// Fails or causes routing issues
// Anti-pattern 3: Non-ASCII characters in IDs
var doc2 = new Document
{
Id = "レポート_2026_データ", // Non-ASCII - interoperability risk
Content = "..."
};
// Works in some SDKs but may break in ADF, Spark, Kafka connectorsCorrect (safe, bounded IDs):
// Use GUIDs or short alphanumeric identifiers
public class Document
{
public string Id { get; set; }
public string Category { get; set; }
public string Title { get; set; }
}
// Option 1: GUID-based IDs (always safe, always unique)
var doc = new Document
{
Id = Guid.NewGuid().ToString(), // "a1b2c3d4-e5f6-..."
Category = "reports",
Title = "Q1 Report"
};
// Option 2: Compact, deterministic IDs from business keys
var doc2 = new Document
{
Id = $"report-{tenantId}-{DateTime.UtcNow:yyyyMMdd}-{sequenceNum}",
Category = "reports",
Title = "Q1 Report"
};
// Option 3: Base64-encode when you must derive from non-ASCII data
var rawId = "レポート_2026_データ";
var doc3 = new Document
{
Id = Convert.ToBase64String(Encoding.UTF8.GetBytes(rawId))
.Replace('/', '_').Replace('+', '-'), // URL-safe Base64
Category = "reports",
Title = rawId // Keep original value as a property
};Key constraints:
- Max length: 1,023 bytes
- Forbidden characters:
/and\are not allowed - Best practice: Use only alphanumeric ASCII characters (
a-z,A-Z,0-9,-,_) - Why: Some SDK versions, Azure Data Factory, Spark connector, and Kafka connector have known issues with non-alphanumeric IDs
- Encode non-ASCII IDs with Base64 + custom encoding if needed for interoperability
Handle JSON Serialization Correctly for Cosmos DB
Cosmos DB stores documents as JSON. Every field on an entity that must be persisted needs to be serializable. Incorrect use of @JsonIgnore, missing constructors, or incompatible field types (like BigDecimal on JDK 17+) cause silent data loss or runtime failures.
Incorrect (common serialization mistakes):
@Container(containerName = "users")
public class User {
@Id
private String id;
@PartitionKey
private String partitionKey = "user";
private String login;
@JsonIgnore // ❌ WRONG: Password will NOT be saved to Cosmos DB
private String password;
@JsonIgnore // ❌ WRONG: Authorities will NOT be saved to Cosmos DB
private Set<String> authorities = new HashSet<>();
private BigDecimal accountBalance; // ❌ Fails on JDK 17+ with reflection errors
}Correct (proper serialization for Cosmos DB):
@Container(containerName = "users")
public class User {
@Id
private String id;
@PartitionKey
private String partitionKey = "user";
private String login;
// ✅ No @JsonIgnore — field is persisted to Cosmos DB
private String password;
// ✅ Use @JsonProperty for explicit field naming, NOT @JsonIgnore
@JsonProperty("authorities")
private Set<String> authorities = new HashSet<>();
// ✅ Use Double instead of BigDecimal for JDK 17+ compatibility
private Double accountBalance;
}Rule 1: Never `@JsonIgnore` persisted fields
@JsonIgnore prevents a field from being written to Cosmos DB. This is the #1 cause of "Cannot pass null or empty values to constructor" errors after reading a document back:
// ❌ Data loss: field is not stored in Cosmos
@JsonIgnore
private String password;
// ✅ Field is stored in Cosmos
private String password;
// ✅ Rename in JSON but still store
@JsonProperty("pwd")
private String password;Only use `@JsonIgnore` on transient/computed fields that should NOT be stored in Cosmos DB (e.g., hydrated relationship objects — see model-relationship-references).
Rule 2: BigDecimal fails on JDK 17+
Java 17+ module system restricts reflection access to BigDecimal internal fields during Jackson serialization:
Unable to make field private final java.math.BigInteger
java.math.BigDecimal.intVal accessibleSolutions (in order of preference):
1. Replace with `Double` — sufficient for most use cases:
private Double amount; // Instead of BigDecimal2. Replace with `String` — for high-precision requirements:
private String amount; // Store "1500.00"
public BigDecimal getAmountAsBigDecimal() {
return new BigDecimal(amount);
}3. Add JVM argument — if BigDecimal must be kept:
--add-opens java.base/java.math=ALL-UNNAMEDRule 3: Provide a default constructor
Cosmos DB deserialization requires a no-arg constructor. If you add parameterized constructors, always keep the default:
@Container(containerName = "items")
public class Item {
// ✅ Default constructor required for deserialization
public Item() {}
public Item(String name, Double price) {
this.name = name;
this.price = price;
}
}Rule 4: Store complex objects as simple types
For complex Cosmos DB compatibility, prefer simple types over JPA entity references:
// ❌ Complex nested entity — may cause serialization issues
private Set<Authority> authorities;
// ✅ Simple string set — reliable serialization
private Set<String> authorities;Convert between simple and complex types in the service layer, not in the entity.
Reference: Jackson annotations guide
Stay Within 128-Level Nesting Depth Limit
Azure Cosmos DB allows a maximum of 128 levels of nesting for embedded objects and arrays. While 128 is generous, recursive or auto-generated structures can exceed this limit unexpectedly.
Incorrect (risk of exceeding nesting limit):
// Anti-pattern 1: Recursive tree stored as deeply nested JSON
public class TreeNode
{
public string Id { get; set; }
public string Name { get; set; }
// Recursive children - each level adds nesting depth
public List<TreeNode> Children { get; set; }
}
// A category hierarchy with 130+ levels will fail on write
var root = BuildDeepTree(depth: 150); // Exceeds 128 levels!
await container.CreateItemAsync(root);
// Microsoft.Azure.Cosmos.CosmosException: Document nesting depth exceeds limit
// Anti-pattern 2: Deeply nested auto-generated JSON from ORMs
// Serializing complex object graphs without cycle detection
var entity = LoadEntityWithAllRelations(); // Lazy-loaded relations
var json = JsonSerializer.Serialize(entity); // May create deep nestingCorrect (bounded nesting depth):
// Solution 1: Flatten deep hierarchies using path-based approach
public class CategoryNode
{
public string Id { get; set; }
public string Name { get; set; }
public string ParentId { get; set; }
// Materialized path captures hierarchy without nesting
public string Path { get; set; } // e.g., "/root/electronics/phones/android"
public int Depth { get; set; }
// Only store immediate children IDs, not nested objects
public List<string> ChildIds { get; set; }
}
// Each node is a flat document, hierarchy expressed via Path and ParentId
var node = new CategoryNode
{
Id = "cat-android",
Name = "Android",
ParentId = "cat-phones",
Path = "/root/electronics/phones/android",
Depth = 3,
ChildIds = new List<string> { "cat-samsung", "cat-pixel" }
};// Solution 2: Cap nesting depth when building recursive structures
public class TreeNode
{
public string Id { get; set; }
public string Name { get; set; }
public List<TreeNode> Children { get; set; }
}
// Limit nesting at serialization time
public static TreeNode TruncateTree(TreeNode node, int maxDepth, int currentDepth = 0)
{
if (currentDepth >= maxDepth || node.Children == null)
{
node.Children = null; // Stop nesting here
return node;
}
node.Children = node.Children
.Select(c => TruncateTree(c, maxDepth, currentDepth + 1))
.ToList();
return node;
}
// Keep well under 128 - aim for practical limits like 10-20
var safeTree = TruncateTree(root, maxDepth: 20);
await container.CreateItemAsync(safeTree);Key points:
- Maximum nesting depth is 128 levels for embedded objects/arrays
- Recursive data structures (trees, graphs) are the most common cause of violations
- Prefer flat representations with references (parent IDs, materialized paths) for deep hierarchies
- If nesting is required, enforce a practical depth cap well under 128
Understand IEEE 754 Numeric Precision Limits
Azure Cosmos DB stores numbers using IEEE 754 double-precision 64-bit format. This means integers larger than 2^53 and decimals requiring more than ~15-17 significant digits will lose precision silently.
Incorrect (precision loss with large numbers):
// Anti-pattern 1: Storing large integers that exceed safe range
public class Transaction
{
public string Id { get; set; }
// 64-bit integer IDs from external systems - DANGER!
public long ExternalTransactionId { get; set; } // e.g., 9007199254740993
// Values > 9,007,199,254,740,992 (2^53) lose precision
// 9007199254740993 becomes 9007199254740992 silently!
}
// Anti-pattern 2: Financial calculations requiring exact decimal precision
public class Invoice
{
public string Id { get; set; }
// Double can't represent all decimal values exactly
public double Amount { get; set; } // 0.1 + 0.2 != 0.3 in IEEE 754
public double TaxRate { get; set; }
}
// 99999999999999.99 stored as double may become 99999999999999.98Correct (preserving precision):
// Solution 1: Store large integers and precise decimals as strings
public class Transaction
{
public string Id { get; set; }
// Store large IDs as strings to preserve all digits
[JsonPropertyName("externalTransactionId")]
public string ExternalTransactionId { get; set; } // "9007199254740993"
}
// Solution 2: Use string representation for financial amounts
public class Invoice
{
public string Id { get; set; }
// Store monetary values as strings with fixed decimal places
[JsonPropertyName("amount")]
public string Amount { get; set; } // "99999999999999.99"
[JsonPropertyName("taxRate")]
public string TaxRate { get; set; } // "0.0825"
// Parse in application code for calculations
public decimal GetAmount() => decimal.Parse(Amount);
public decimal GetTaxRate() => decimal.Parse(TaxRate);
}// Solution 3: Store amounts as integer minor units (cents, paise, etc.)
public class Payment
{
public string Id { get; set; }
// Store $199.99 as 19999 cents - always safe as integer within 2^53
public long AmountInCents { get; set; }
public string Currency { get; set; } // "USD"
// Helper for display
public decimal GetDisplayAmount() => AmountInCents / 100m;
}
var payment = new Payment
{
Id = Guid.NewGuid().ToString(),
AmountInCents = 19999, // $199.99
Currency = "USD"
};
await container.CreateItemAsync(payment);Key points:
- Safe integer range: -2^53 to 2^53 (±9,007,199,254,740,992)
- Significant digits: ~15-17 decimal digits of precision
- Store large integers (snowflake IDs, blockchain hashes) as strings
- Store financial/monetary values as strings or integer minor units (cents)
- Numbers within the safe range (most counters, ages, quantities) are fine as-is
Reference Data When Items Grow Large
Use document references instead of embedding when embedded data would make items too large, or when embedded data changes independently.
Incorrect (embedded array grows unbounded):
// Anti-pattern: blog post with all comments embedded
public class BlogPost
{
public string Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
// This array can grow forever - will eventually hit 2MB limit!
public List<Comment> Comments { get; set; } // Could be thousands
}
// Eventually fails when document exceeds 2MB
await container.UpsertItemAsync(blogPost);
// RequestEntityTooLarge exceptionCorrect (reference pattern for unbounded relationships):
// Blog post document (bounded size)
public class BlogPost
{
public string Id { get; set; }
public string PostId { get; set; } // Partition key
public string Type { get; set; } = "post";
public string Title { get; set; }
public string Content { get; set; }
public int CommentCount { get; set; } // Denormalized count
}
// Separate comment documents (same partition for efficient queries)
public class Comment
{
public string Id { get; set; }
public string PostId { get; set; } // Partition key - same as post
public string Type { get; set; } = "comment";
public string AuthorId { get; set; }
public string Text { get; set; }
public DateTime CreatedAt { get; set; }
}
// Query comments within same partition - efficient!
var comments = container.GetItemQueryIterator<Comment>(
new QueryDefinition("SELECT * FROM c WHERE c.postId = @postId AND c.type = 'comment' ORDER BY c.createdAt DESC")
.WithParameter("@postId", postId),
requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey(postId) }
);Use references when:
- Embedded data is unbounded (arrays that grow)
- Embedded data changes frequently/independently
- You need to query embedded data separately
Reference: Model document data
Use ID References with Transient Hydration for Document Relationships
Cosmos DB has no cross-document JOINs. When entities need to reference each other, store relationship IDs as persistent fields and use transient (@JsonIgnore) properties for hydrated object access. A service layer populates the transient properties before rendering.
This pattern goes beyond basic referencing (see model-reference-large) by providing a complete strategy for applications that need both document storage efficiency and runtime object graphs (e.g., web apps with templates, REST APIs returning nested objects).
Incorrect (JPA relationship annotations — no Cosmos equivalent):
@Entity
public class Vet {
@Id
private Integer id;
@ManyToMany
@JoinTable(name = "vet_specialties")
private List<Specialty> specialties; // JPA manages this relationship
}Also incorrect (embedding unbounded relationships directly):
@Container(containerName = "vets")
public class Vet {
@Id
private String id;
// ❌ Stores full Specialty objects — grows unbounded, duplicates data
private List<Specialty> specialties;
}Correct (ID references + transient hydration):
@Container(containerName = "vets")
public class Vet {
@Id
@GeneratedValue
private String id;
@PartitionKey
private String partitionKey = "vet";
private String firstName;
private String lastName;
// ✅ Persisted to Cosmos DB — stores only IDs
private List<String> specialtyIds = new ArrayList<>();
// ✅ Transient — NOT stored in Cosmos DB, populated by service layer
@JsonIgnore
private List<Specialty> specialties = new ArrayList<>();
// Both getters needed
public List<String> getSpecialtyIds() { return specialtyIds; }
public List<Specialty> getSpecialties() { return specialties; }
// Count methods should use the transient list when populated,
// fall back to ID list
public int getNrOfSpecialties() {
return specialties.isEmpty() ? specialtyIds.size() : specialties.size();
}
}When to use this pattern:
| Scenario | Approach |
|---|---|
| Related data always read together, bounded size | Embed (see model-embed-related) |
| Related data read independently, unbounded | ID reference (this pattern) |
| UI/template needs object access to related data | ID reference + transient hydration (this pattern) |
| REST API returns nested objects | ID reference + transient hydration (this pattern) |
| Related data rarely accessed after write | ID reference only (no transient needed) |
The transient hydration flow:
1. Entity stores List<String> specialtyIds (persisted) 2. Service layer reads the entity, then looks up each ID to get full objects 3. Service populates List<Specialty> specialties (transient) 4. Controller/template accesses vet.getSpecialties() as if it were a normal object graph
Important: @JsonIgnore is correct here because transient properties should NOT be stored in Cosmos DB — they are populated on read by the service layer. This is the one legitimate use of @JsonIgnore (see model-json-serialization for when NOT to use it).
Reference: Data modeling in Azure Cosmos DB
Version Your Document Schemas
Include schema version in documents to handle evolution gracefully. This enables safe migrations and backward-compatible reads.
Incorrect (no version tracking):
// Original schema
public class UserV1
{
public string Id { get; set; }
public string Name { get; set; } // Later split into FirstName + LastName
public string Address { get; set; } // Later becomes Address object
}
// After schema change, old documents break deserialization
public class User
{
public string Id { get; set; }
public string FirstName { get; set; } // Null for old docs!
public string LastName { get; set; } // Null for old docs!
public Address Address { get; set; } // Deserialization fails!
}Correct (versioned documents):
public abstract class UserBase
{
public string Id { get; set; }
public int SchemaVersion { get; set; }
}
public class UserV1 : UserBase
{
public string Name { get; set; }
public string Address { get; set; }
}
public class UserV2 : UserBase
{
public string FirstName { get; set; }
public string LastName { get; set; }
public AddressV2 Address { get; set; }
}
// Read with version handling
public async Task<User> GetUserAsync(string id, string partitionKey)
{
var response = await container.ReadItemStreamAsync(id, new PartitionKey(partitionKey));
using var doc = await JsonDocument.ParseAsync(response.Content);
var version = doc.RootElement.GetProperty("schemaVersion").GetInt32();
return version switch
{
1 => MigrateV1ToV2(JsonSerializer.Deserialize<UserV1>(doc)),
2 => JsonSerializer.Deserialize<UserV2>(doc),
_ => throw new NotSupportedException($"Unknown schema version: {version}")
};
}
// Background migration using Change Feed
public async Task MigrateUserDocuments()
{
var changeFeed = container.GetChangeFeedProcessorBuilder<UserV1>("migration", HandleChanges)
.WithInstanceName("migrator")
.WithStartTime(DateTime.MinValue.ToUniversalTime())
.Build();
await changeFeed.StartAsync();
}Always increment version when:
- Adding required fields
- Changing field types
- Restructuring nested objects
Reference: Schema evolution in Cosmos DB
Use Type Discriminators for Polymorphic Data
Use a single Cosmos DB container to co-locate related parent/child or different entity types when:
- similar entities are written and read together, share a natural or business partition key, require a simple transactional boundary, and do not exceed Cosmos DB partition key limits.
When storing multiple entity types in the same container, include a type discriminator field for efficient filtering and deserialization.
Incorrect (no type discrimination):
// Multiple types in same container without clear identification
public class Order { public string Id { get; set; } /* ... */ }
public class Customer { public string Id { get; set; } /* ... */ }
public class Product { public string Id { get; set; } /* ... */ }
// How do you query just orders? Full scan!
var allItems = await container.GetItemQueryIterator<dynamic>("SELECT * FROM c").ReadNextAsync();
var orders = allItems.Where(x => x.orderDate != null); // Brittle, inefficientCorrect (explicit type discriminator):
// Base class with type discriminator
public abstract class BaseEntity
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("type")]
public abstract string Type { get; }
[JsonPropertyName("partitionKey")]
public string PartitionKey { get; set; }
}
public class Order : BaseEntity
{
public override string Type => "order";
public DateTime OrderDate { get; set; }
public List<OrderItem> Items { get; set; }
}
public class Customer : BaseEntity
{
public override string Type => "customer";
public string Email { get; set; }
public string Name { get; set; }
}
public class Product : BaseEntity
{
public override string Type => "product";
public string Name { get; set; }
public decimal Price { get; set; }
}
// Efficient queries by type - uses index!
var ordersQuery = new QueryDefinition(
"SELECT * FROM c WHERE c.type = @type AND c.partitionKey = @pk")
.WithParameter("@type", "order")
.WithParameter("@pk", customerId);
// Polymorphic deserialization
public static BaseEntity DeserializeEntity(JsonDocument doc)
{
var type = doc.RootElement.GetProperty("type").GetString();
return type switch
{
"order" => doc.Deserialize<Order>(),
"customer" => doc.Deserialize<Customer>(),
"product" => doc.Deserialize<Product>(),
_ => throw new InvalidOperationException($"Unknown type: {type}")
};
}Benefits:
- Efficient filtering with indexed
typefield - Clear deserialization logic
- Self-documenting data structure
When NOT to Use Multi-Entity Containers :
- Independent throughput requirements → Use separate containers
- Different scaling patterns → Use separate containers
- Different indexing needs → Use separate containers
- Distinct change feed processing requirements → Use separate containers
- Low access correlation (<20%) → Use separate containers
Single-Container Anti-Patterns :
- "Everything container" → Complex filtering → Difficult analytics
- One throughput allocation for all entity types
- One change feed with mixed events requiring filtering
- Difficult to maintain and onboard new developers
Reference: Model data in Cosmos DB
Integrate Azure Monitor
Enable Azure Monitor integration for comprehensive visibility into Cosmos DB performance, availability, and cost metrics.
Incorrect (no monitoring integration):
// Flying blind - no visibility into:
// - RU consumption trends
// - Latency patterns
// - Throttling events
// - Availability issues
// - Cost attribution
// Application runs but you only know about problems from user complaintsCorrect (Azure Monitor integration):
// Step 1: Enable diagnostic settings (Azure Portal, CLI, or ARM)
{
"type": "Microsoft.DocumentDB/databaseAccounts/providers/diagnosticSettings",
"properties": {
"logs": [
{
"category": "DataPlaneRequests",
"enabled": true,
"retentionPolicy": { "enabled": true, "days": 30 }
},
{
"category": "QueryRuntimeStatistics",
"enabled": true
},
{
"category": "PartitionKeyStatistics",
"enabled": true
},
{
"category": "PartitionKeyRUConsumption",
"enabled": true
}
],
"metrics": [
{
"category": "Requests",
"enabled": true
}
],
"workspaceId": "/subscriptions/.../workspaces/my-workspace"
}
}// Step 2: Key metrics to monitor in Azure Monitor
// a) Normalized RU Consumption (% of provisioned used)
// Alert if > 90% sustained - indicates need to scale
// b) Total Requests by Status Code
// Alert on 429s (throttling) and 5xx (errors)
// c) Server Side Latency
// Track P50, P99 for performance baselines
// d) Data Usage
// Monitor storage growth
// e) Availability
// Alert on availability drops below 99.99%// Step 3: Application Insights integration
public static class CosmosDbTelemetry
{
public static void ConfigureWithAppInsights(
CosmosClientOptions options,
TelemetryClient telemetry)
{
// Track all operations as dependencies
options.CosmosClientTelemetryOptions = new CosmosClientTelemetryOptions
{
DisableDistributedTracing = false // Enable distributed tracing
};
// Custom handler for detailed telemetry
options.CustomHandlers.Add(new AppInsightsHandler(telemetry));
}
}
public class AppInsightsHandler : RequestHandler
{
private readonly TelemetryClient _telemetry;
public override async Task<ResponseMessage> SendAsync(
RequestMessage request,
CancellationToken cancellationToken)
{
using var operation = _telemetry.StartOperation<DependencyTelemetry>(
"CosmosDB",
request.RequestUri.ToString());
operation.Telemetry.Type = "Azure DocumentDB";
operation.Telemetry.Target = request.RequestUri.Host;
var response = await base.SendAsync(request, cancellationToken);
operation.Telemetry.Success = response.IsSuccessStatusCode;
operation.Telemetry.ResultCode = ((int)response.StatusCode).ToString();
operation.Telemetry.Properties["RU"] = response.Headers.RequestCharge.ToString();
return response;
}
}// Useful Log Analytics queries
// RU consumption by operation
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.DOCUMENTDB"
| summarize TotalRU = sum(requestCharge_s),
AvgRU = avg(requestCharge_s),
Count = count()
by OperationName
| order by TotalRU desc
// Slow queries
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.DOCUMENTDB"
| where duration_s > 100 // > 100ms
| project TimeGenerated, OperationName, duration_s,
requestCharge_s, partitionKey_s, querytext_s
// Storage growth trend
AzureMetrics
| where ResourceProvider == "MICROSOFT.DOCUMENTDB"
| where MetricName == "DataUsage"
| summarize StorageGB = max(Total) / 1073741824 by bin(TimeGenerated, 1d)
| order by TimeGeneratedEssential alerts to configure: 1. Throttling (429s) > 0 2. Normalized RU > 90% for 5 min 3. Availability < 99.99% 4. P99 latency > threshold 5. Storage approaching limits
Reference: Monitor Azure Cosmos DB
Enable Diagnostic Logging
Enable diagnostic logging to capture detailed operation data for troubleshooting. Essential for root cause analysis of production issues.
Incorrect (no diagnostic logging):
// When issues occur, you have no data to investigate
// "Why is this query slow?"
// "Why did we get throttled yesterday at 3am?"
// "Which operations are using the most RU?"
// No answers without logging!Correct (comprehensive diagnostic logging):
// Azure diagnostic settings for detailed logs
// Enable via Azure Portal > Cosmos DB > Diagnostic settings
// Categories to enable:
// 1. DataPlaneRequests - All CRUD operations
// 2. QueryRuntimeStatistics - Query execution details
// 3. PartitionKeyStatistics - Partition key distribution
// 4. PartitionKeyRUConsumption - RU by partition
// 5. ControlPlaneRequests - Management operations
// ARM template for diagnostic settings
{
"type": "Microsoft.Insights/diagnosticSettings",
"name": "cosmos-diagnostics",
"properties": {
"logs": [
{ "category": "DataPlaneRequests", "enabled": true },
{ "category": "QueryRuntimeStatistics", "enabled": true },
{ "category": "PartitionKeyStatistics", "enabled": true },
{ "category": "PartitionKeyRUConsumption", "enabled": true },
{ "category": "ControlPlaneRequests", "enabled": true }
],
"logAnalyticsDestinationType": "Dedicated",
"workspaceId": "[resourceId('Microsoft.OperationalInsights/workspaces', 'my-workspace')]"
}
}// Application-level diagnostic logging
public class DiagnosticLoggingRepository
{
private readonly Container _container;
private readonly ILogger _logger;
public async Task<T> ExecuteWithDiagnostics<T>(
string operationName,
Func<Task<Response<T>>> operation)
{
var correlationId = Activity.Current?.Id ?? Guid.NewGuid().ToString();
try
{
var response = await operation();
// Always log basic info
_logger.LogDebug(
"[{CorrelationId}] {Operation}: {RU} RU, {LatencyMs}ms, Status: {Status}",
correlationId,
operationName,
response.RequestCharge,
response.Diagnostics.GetClientElapsedTime().TotalMilliseconds,
"Success");
// Log full diagnostics for slow operations
if (response.Diagnostics.GetClientElapsedTime() > TimeSpan.FromMilliseconds(100))
{
_logger.LogWarning(
"[{CorrelationId}] Slow {Operation}: {Diagnostics}",
correlationId,
operationName,
response.Diagnostics.ToString());
}
return response.Resource;
}
catch (CosmosException ex)
{
_logger.LogError(ex,
"[{CorrelationId}] {Operation} failed: Status={Status}, SubStatus={SubStatus}, " +
"RU={RU}, RetryAfter={RetryAfter}, ActivityId={ActivityId}, Diagnostics={Diagnostics}",
correlationId,
operationName,
ex.StatusCode,
ex.SubStatusCode,
ex.RequestCharge,
ex.RetryAfter,
ex.ActivityId,
ex.Diagnostics?.ToString());
throw;
}
}
}// Query-specific diagnostics
public async Task<List<T>> ExecuteQueryWithDiagnostics<T>(
string queryName,
QueryDefinition query,
QueryRequestOptions options = null)
{
options ??= new QueryRequestOptions();
options.PopulateIndexMetrics = true; // Get index usage info
var results = new List<T>();
var totalRU = 0.0;
var pageCount = 0;
var iterator = _container.GetItemQueryIterator<T>(query, requestOptions: options);
while (iterator.HasMoreResults)
{
var response = await iterator.ReadNextAsync();
results.AddRange(response);
totalRU += response.RequestCharge;
pageCount++;
// Log index metrics (helps identify missing indexes)
if (!string.IsNullOrEmpty(response.IndexMetrics))
{
_logger.LogDebug(
"Query '{QueryName}' page {Page} index metrics: {IndexMetrics}",
queryName, pageCount, response.IndexMetrics);
}
}
_logger.LogInformation(
"Query '{QueryName}': {Count} results, {TotalRU} RU, {Pages} pages",
queryName, results.Count, totalRU, pageCount);
return results;
}Key diagnostic data to capture:
- Operation name and duration
- RU consumption
- Partition key (for hot partition analysis)
- Full diagnostics for errors/slow operations
- Index metrics for queries
- ActivityId (for Azure support)
Reference: Diagnostic logging
Monitor P99 Latency
Track P99 (99th percentile) latency to identify performance outliers. Average latency hides tail latency issues that affect user experience.
Incorrect (only tracking average latency):
// Average latency looks good: 5ms
// But P99 could be 500ms - 1% of users have terrible experience!
public async Task<Order> GetOrder(string orderId, string customerId)
{
var sw = Stopwatch.StartNew();
var result = await _container.ReadItemAsync<Order>(orderId, pk);
sw.Stop();
// Only tracking average is misleading
_metrics.TrackAverage("CosmosDB.Latency", sw.ElapsedMilliseconds);
// Average: 5ms (hides that some requests take 500ms)
return result.Resource;
}Correct (tracking latency distribution):
public async Task<Order> GetOrder(string orderId, string customerId)
{
var sw = Stopwatch.StartNew();
var response = await _container.ReadItemAsync<Order>(orderId, new PartitionKey(customerId));
sw.Stop();
var clientLatency = sw.ElapsedMilliseconds;
var serverLatency = response.Diagnostics.GetClientElapsedTime().TotalMilliseconds;
// Track as histogram (enables percentile calculations)
_metrics.TrackHistogram("CosmosDB.Latency.Client", clientLatency);
_metrics.TrackHistogram("CosmosDB.Latency.Server", serverLatency);
// Alert on slow requests
if (clientLatency > 100) // 100ms threshold
{
_logger.LogWarning(
"Slow Cosmos DB read: {LatencyMs}ms, Diagnostics: {Diagnostics}",
clientLatency,
response.Diagnostics.ToString());
}
return response.Resource;
}// Track percentiles with Application Insights
public class LatencyTracker
{
private readonly TelemetryClient _telemetry;
private readonly ConcurrentBag<double> _recentLatencies = new();
private readonly Timer _reportTimer;
public LatencyTracker(TelemetryClient telemetry)
{
_telemetry = telemetry;
_reportTimer = new Timer(ReportPercentiles, null,
TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
}
public void RecordLatency(double latencyMs)
{
_recentLatencies.Add(latencyMs);
}
private void ReportPercentiles(object state)
{
var latencies = _recentLatencies.ToArray();
_recentLatencies.Clear();
if (latencies.Length == 0) return;
Array.Sort(latencies);
var p50 = GetPercentile(latencies, 50);
var p90 = GetPercentile(latencies, 90);
var p99 = GetPercentile(latencies, 99);
_telemetry.TrackMetric("CosmosDB.Latency.P50", p50);
_telemetry.TrackMetric("CosmosDB.Latency.P90", p90);
_telemetry.TrackMetric("CosmosDB.Latency.P99", p99);
// Alert if P99 exceeds threshold
if (p99 > 100)
{
_telemetry.TrackEvent("HighP99Latency",
new Dictionary<string, string> { { "P99", p99.ToString() } });
}
}
private static double GetPercentile(double[] sorted, int percentile)
{
var index = (int)Math.Ceiling(percentile / 100.0 * sorted.Length) - 1;
return sorted[Math.Max(0, index)];
}
}// Azure Monitor / Log Analytics query for P99
// Query to get latency percentiles
/*
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.DOCUMENTDB"
| where TimeGenerated > ago(1h)
| summarize
P50 = percentile(duration_s, 50),
P90 = percentile(duration_s, 90),
P99 = percentile(duration_s, 99),
Max = max(duration_s)
by bin(TimeGenerated, 5m), OperationName
| order by TimeGenerated desc
*/What P99 latency reveals:
- Network issues (high client vs server latency gap)
- Hot partitions (certain keys slow)
- Query efficiency problems
- Cross-partition query overhead
- Regional routing issues
Target latencies:
- Point reads: P99 < 10ms (same region)
- Queries: P99 < 50ms (depends on complexity)
- Cross-region: Add ~RTT to target
Reference: Monitor latency
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):
// 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):
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;
}// 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;
}// 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) }
});Azure Monitor queries for RU analysis:
// 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 descReference: Monitor RU/s
Alert on Throttling (429s)
Set up alerts for HTTP 429 (Request Rate Too Large) errors. Throttling indicates your application is exceeding provisioned throughput.
Incorrect (ignoring throttling):
// SDK retries silently, application seems "slow" but no alerts
public async Task<Order> GetOrder(string orderId, string customerId)
{
// SDK retries 429s automatically (up to 9 times by default)
// But you have no visibility into this happening!
return await _container.ReadItemAsync<Order>(orderId, new PartitionKey(customerId));
// Users experience slow responses, you see nothing in logs
}Correct (tracking and alerting on throttling):
// Option 1: Track via exception handling
public async Task<Order> GetOrder(string orderId, string customerId)
{
try
{
var response = await _container.ReadItemAsync<Order>(orderId, new PartitionKey(customerId));
return response.Resource;
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
// This fires only after ALL retries exhausted
_logger.LogError(
"Throttled after max retries! RetryAfter: {RetryAfter}s, Diagnostics: {Diagnostics}",
ex.RetryAfter?.TotalSeconds,
ex.Diagnostics?.ToString());
_metrics.IncrementCounter("CosmosDB.ThrottledRequests");
throw;
}
}
// Option 2: Custom handler to track all 429s (even those retried)
public class ThrottlingTracker : RequestHandler
{
private readonly ILogger _logger;
private readonly IMetricTracker _metrics;
public override async Task<ResponseMessage> SendAsync(
RequestMessage request,
CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.LogWarning(
"429 Throttled: {Uri}, RetryAfter: {RetryAfter}",
request.RequestUri,
response.Headers.RetryAfter);
_metrics.IncrementCounter("CosmosDB.429.Total");
}
return response;
}
}
// Register handler
var client = new CosmosClient(connectionString, new CosmosClientOptions
{
CustomHandlers = { new ThrottlingTracker(_logger, _metrics) }
});// Azure Monitor alert rule for throttling
// Create alert in Azure Portal or via ARM:
{
"type": "Microsoft.Insights/metricAlerts",
"properties": {
"criteria": {
"odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria",
"allOf": [
{
"name": "TotalRequests429",
"metricName": "TotalRequests",
"dimensions": [
{
"name": "StatusCode",
"operator": "Include",
"values": ["429"]
}
],
"operator": "GreaterThan",
"threshold": 0,
"timeAggregation": "Total"
}
]
},
"actions": [
{
"actionGroupId": "/subscriptions/.../actionGroups/ops-team"
}
],
"severity": 2,
"windowSize": "PT5M",
"evaluationFrequency": "PT1M"
}
}// Log Analytics query for throttling analysis
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.DOCUMENTDB"
| where statusCode_s == "429"
| summarize ThrottledCount = count() by
bin(TimeGenerated, 5m),
partitionKeyRangeId_s,
OperationName
| order by TimeGenerated desc
// Identify which partition keys are throttling
AzureDiagnostics
| where statusCode_s == "429"
| summarize Count = count() by partitionKey_s
| order by Count desc
| take 10Response to throttling: 1. Immediate: SDK retries automatically 2. Short-term: Scale up throughput (manual or autoscale) 3. Long-term:
- Optimize queries to use less RU
- Review partition key for hot partitions
- Consider autoscale for variable workloads
Reference: Monitor throttling
Plan for 20GB Logical Partition Limit
Each logical partition has a 20GB storage limit. Design partition keys to ensure no single partition value accumulates more than 20GB.
Incorrect (unbounded partition growth):
// Anti-pattern: partition key with unbounded data accumulation
public class AuditLog
{
public string Id { get; set; }
public string SystemId { get; set; } // Partition key - only 3 systems!
public DateTime Timestamp { get; set; }
public string Action { get; set; }
public string Details { get; set; }
}
// Problem: Each system accumulates logs forever
// "system-a" partition will eventually hit 20GB
// Writes will fail with: PartitionKeyRangeIsFullCorrect (bounded partition growth):
// Solution 1: Time-bucket the partition key
public class AuditLog
{
public string Id { get; set; }
public string SystemId { get; set; }
public DateTime Timestamp { get; set; }
// Partition by system + month
public string PartitionKey => $"{SystemId}_{Timestamp:yyyy-MM}";
}
// Each partition holds ~1 month of data per system
// Old partitions naturally stop growing// Solution 2: Use hierarchical partition keys
var containerProperties = new ContainerProperties
{
Id = "audit-logs",
PartitionKeyPaths = new List<string>
{
"/systemId",
"/yearMonth" // Secondary level prevents 20GB limit
}
};
public class AuditLog
{
public string Id { get; set; }
public string SystemId { get; set; }
public string YearMonth { get; set; } // "2026-01"
public DateTime Timestamp { get; set; }
}// Monitor partition sizes
public async Task CheckPartitionSizes()
{
var partitionKeyRanges = container.GetFeedRanges();
foreach (var range in await partitionKeyRanges)
{
var iterator = container.GetItemQueryIterator<dynamic>(
"SELECT * FROM c",
requestOptions: new QueryRequestOptions { FeedRange = range });
// Check size via metrics or diagnostic headers
var response = await iterator.ReadNextAsync();
_logger.LogInformation(
"Partition {Range}: {Count} items, {RU} RU",
range, response.Count, response.RequestCharge);
}
}
// Set up alerts before hitting limits
// Azure Monitor: PartitionKeyRangeId with high storageCapacity planning:
- Estimate item count per partition key value
- Calculate average item size × item count
- Target < 10GB per partition value (50% safety margin)
- Consider time-based bucketing for growing data
Reference: Partition key limits
Distribute Writes to Avoid Hot Partitions
Ensure writes distribute evenly across partitions. A hot partition limits throughput to that single partition's capacity.
Incorrect (all writes hit single partition):
// Anti-pattern: time-based partition key with current-time writes
public class Event
{
public string Id { get; set; }
// All events for "today" go to same partition!
public string Date { get; set; } // ❌ "2026-01-21" - HOT!
}
// All current writes bottleneck on today's partition
// Yesterday's partition sits idle
await container.CreateItemAsync(new Event
{
Id = Guid.NewGuid().ToString(),
Date = DateTime.UtcNow.ToString("yyyy-MM-dd") // All writes here!
});// Anti-pattern: singleton partition key
public class Config
{
public string Id { get; set; }
public string PartitionKey { get; set; } = "config"; // ❌ ONE partition!
}
// Everything in single 10K RU/s max partitionCorrect (distributed writes):
// Good: write-sharding for time-series data
public class Event
{
public string Id { get; set; }
// Combine date with hash suffix for distribution
public string PartitionKey { get; set; } // "2026-01-21_shard3"
}
public static string CreateTimeShardedKey(DateTime timestamp, int shardCount = 10)
{
var dateKey = timestamp.ToString("yyyy-MM-dd");
var shard = Math.Abs(Guid.NewGuid().GetHashCode()) % shardCount;
return $"{dateKey}_shard{shard}";
}
// Writes distribute across 10 partitions per day
await container.CreateItemAsync(new Event
{
Id = Guid.NewGuid().ToString(),
PartitionKey = CreateTimeShardedKey(DateTime.UtcNow)
});// Good: natural distribution with entity IDs
public class Order
{
public string Id { get; set; }
public string CustomerId { get; set; } // ✅ Natural distribution
public DateTime OrderDate { get; set; }
}
// Each customer's orders in their own partition
// Writes naturally spread across many customersMonitor for hot partitions:
- Check Metrics → Normalized RU Consumption
- Look for partitions consistently at 100%
- Use Azure Monitor alerts for throttling
Partition Limits (as of current Azure Cosmos DB documentation):
- Physical partition throughput limit: 10,000 RU/s per physical partition
See Azure Cosmos DB partitioning – physical partitions.
- Logical partition size limit: 20 GB per logical partition
See Azure Cosmos DB partitioning – logical partitions.
- Physical partition size: 50 GB per physical partition
See Azure Cosmos DB partitioning – physical partitions.
These limits can evolve over time and may vary by region/offer. Always confirm against the latest Azure Cosmos DB documentation for your account.
Popularity Skew Warning for Hot Partitions: Even high-cardinality keys (like user_id) can create hot partitions when specific values get dramatically more traffic (e.g., a viral user during peak moments).
Use Hierarchical Partition Keys for Flexibility
Use hierarchical partition keys (HPK) to overcome the 20GB logical partition limit and enable targeted multi-partition queries.
Incorrect (single-level hits 20GB limit):
// Problem: Large tenant exceeds 20GB logical partition limit
public class Document
{
public string Id { get; set; }
public string TenantId { get; set; } // Single partition key
// Large tenants hit 20GB ceiling!
}
// Must spread tenant data manually
// Queries across "big-tenant_shard1", "big-tenant_shard2" are complexCorrect (hierarchical partition keys):
// Create container with hierarchical partition key
var containerProperties = new ContainerProperties
{
Id = "documents",
PartitionKeyPaths = new List<string>
{
"/tenantId", // Level 1: Tenant
"/year", // Level 2: Year
"/month" // Level 3: Month (optional)
}
};
await database.CreateContainerAsync(containerProperties, throughput: 10000);
// Document with hierarchical key
public class Document
{
public string Id { get; set; }
public string TenantId { get; set; }
public int Year { get; set; }
public int Month { get; set; }
public string Content { get; set; }
}
// Query targeting specific levels
// Level 1 only: scans all partitions for tenant
var tenantDocs = container.GetItemQueryIterator<Document>(
new QueryDefinition("SELECT * FROM c WHERE c.tenantId = @tenant")
.WithParameter("@tenant", "acme-corp"));
// Level 1+2: targets specific year partitions
var yearDocs = container.GetItemQueryIterator<Document>(
new QueryDefinition("SELECT * FROM c WHERE c.tenantId = @tenant AND c.year = @year")
.WithParameter("@tenant", "acme-corp")
.WithParameter("@year", 2026),
requestOptions: new QueryRequestOptions
{
PartitionKey = new PartitionKeyBuilder()
.Add("acme-corp")
.Add(2026)
.Build()
});
// Full key: single partition point read
var doc = await container.ReadItemAsync<Document>(
docId,
new PartitionKeyBuilder()
.Add("acme-corp")
.Add(2026)
.Add(1)
.Build());Benefits of HPK:
- Each level combination creates separate logical partitions (no 20GB limit per tenant)
- Queries can target specific levels for efficiency
- Natural data organization (tenant → year → month)
Reference: Hierarchical partition keys
Choose High-Cardinality Partition Keys
Select partition keys with many unique values to ensure even data distribution. Low-cardinality keys create hot partitions.
Incorrect (low cardinality creates hotspots):
// Anti-pattern: using status as partition key
public class Order
{
public string Id { get; set; }
// Only 5-10 unique values: "pending", "processing", "shipped", "delivered", "cancelled"
public string Status { get; set; } // ❌ BAD partition key!
}
// Result: All "pending" orders in ONE partition
// That partition becomes a hotspot during peak ordering!// Anti-pattern: using country as partition key
public class User
{
public string Id { get; set; }
// Only ~195 countries, uneven distribution
public string Country { get; set; } // ❌ BAD - US/India will be hot
}Correct (high cardinality with even distribution):
// Good: using unique identifier as partition key
public class Order
{
public string Id { get; set; }
// Millions of unique customers = even distribution
public string CustomerId { get; set; } // ✅ GOOD partition key
public string Status { get; set; } // Just a regular property now
}
// Good: using tenant ID for multi-tenant apps
public class Document
{
public string Id { get; set; }
// Each tenant gets their own partition(s)
public string TenantId { get; set; } // ✅ GOOD - natural isolation
}
// Good: using device ID for IoT
public class Telemetry
{
public string Id { get; set; }
// Thousands/millions of devices
public string DeviceId { get; set; } // ✅ GOOD partition key
public DateTime Timestamp { get; set; }
public double Temperature { get; set; }
}Good partition keys typically:
- Have thousands to millions of unique values
- Match your most common query patterns
- Distribute writes evenly (no single key dominates)
Reference: Partitioning in Azure Cosmos DB
Respect Partition Key Value Length Limits
Azure Cosmos DB enforces a maximum partition key value length of 2,048 bytes (or 101 bytes if large partition keys are not enabled). Exceeding this limit causes write failures at runtime.
Incorrect (risk of exceeding partition key length):
// Anti-pattern: concatenating many fields into a partition key
public class Document
{
public string Id { get; set; }
// Partition key built from long descriptions - DANGER!
public string PartitionKey => $"{TenantName}_{DepartmentName}_{TeamName}_{ProjectDescription}";
public string TenantName { get; set; } // Could be very long
public string DepartmentName { get; set; }
public string TeamName { get; set; }
public string ProjectDescription { get; set; } // Unbounded user input
}
// If PartitionKey exceeds 2,048 bytes:
// Microsoft.Azure.Cosmos.CosmosException: Partition key value is too largeCorrect (bounded partition key values):
// Use short, bounded identifiers for partition keys
public class Document
{
public string Id { get; set; }
// Short, deterministic IDs - always well under 2,048 bytes
public string TenantId { get; set; } // e.g., "t-abc123"
public string DepartmentId { get; set; } // e.g., "dept-42"
// Partition key uses compact identifiers
public string PartitionKey => $"{TenantId}_{DepartmentId}";
// Keep long text as regular properties, not in the partition key
public string TenantName { get; set; }
public string DepartmentName { get; set; }
public string ProjectDescription { get; set; }
}// If you must derive a key from long values, hash or truncate them
public class Document
{
public string Id { get; set; }
public string LongCategoryPath { get; set; } // e.g., deep taxonomy
// Hash long values to a fixed-length partition key
public string PartitionKey
{
get
{
using var sha = System.Security.Cryptography.SHA256.Create();
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(LongCategoryPath));
return Convert.ToBase64String(hash)[..16]; // Fixed 16-char key
}
}
}Key points:
- Default limit is 101 bytes without large partition key feature enabled
- With large partition keys enabled, limit increases to 2,048 bytes
- Enable large partition keys for new containers if you need longer values
- Prefer short GUIDs, IDs, or codes over human-readable strings for partition keys
Align Partition Key with Query Patterns
Choose a partition key that supports your most frequent queries. Single-partition queries are orders of magnitude faster than cross-partition.
Incorrect (partition key misaligned with queries):
// Document partitioned by category
public class Product
{
public string Id { get; set; }
public string Category { get; set; } // Partition key
public string SellerId { get; set; }
}
// But most queries are by seller!
// This forces expensive cross-partition scan
var sellerProducts = container.GetItemQueryIterator<Product>(
new QueryDefinition("SELECT * FROM c WHERE c.sellerId = @seller")
.WithParameter("@seller", sellerId));
// Scans ALL partitions - high RU, high latencyCorrect (partition key matches query patterns):
// Step 1: Analyze your query patterns
// - 80% of queries: "Get all products for seller X"
// - 15% of queries: "Get product by ID"
// - 5% of queries: "Get products by category"
// Step 2: Choose partition key for dominant pattern
public class Product
{
public string Id { get; set; }
public string SellerId { get; set; } // Partition key - matches 80% queries!
public string Category { get; set; }
}
// Most common query is now single-partition
var sellerProducts = container.GetItemQueryIterator<Product>(
new QueryDefinition("SELECT * FROM c WHERE c.sellerId = @seller")
.WithParameter("@seller", sellerId),
requestOptions: new QueryRequestOptions
{
PartitionKey = new PartitionKey(sellerId) // Single partition!
});
// Fast, low RU
// For less common category queries, accept cross-partition
// Or create a secondary container partitioned by category// E-commerce example: Orders partitioned by CustomerId
public class Order
{
public string Id { get; set; }
public string CustomerId { get; set; } // Partition key
public DateTime OrderDate { get; set; }
public string Status { get; set; }
}
// "Show my orders" - single partition, fast
// "All orders today" - cross-partition, but rare admin query
// Chat example: Messages partitioned by ConversationId
public class Message
{
public string Id { get; set; }
public string ConversationId { get; set; } // Partition key
public string SenderId { get; set; }
public string Content { get; set; }
}
// "Get messages in conversation" - single partition, fastReference: Choose a partition key
Related skills
FAQ
How many rules does cosmosdb-best-practices contain?
73+ rules across 10 categories, prioritized by impact, with one file per rule loaded on demand.
What areas do the rules cover?
Data modeling, partition key design, query optimization, SDK best practices, and additional categories including vector search and RAG patterns.