Partitioning and consistency choices are effectively permanent and decide Cosmos DB's cost, latency and correctness.
Under the hood a container's data is split into physical partitions, each serving up to 10,000 RU/s and 50 GB. Provisioned RU/s are divided evenly between them, so a hot partition key (say /country where one country dominates) throttles at a fraction of the container's throughput while the rest sits idle. Good keys have high cardinality, spread writes evenly, and match your most frequent queries' filters so they stay single-partition.
Techniques:
- Synthetic keys (
tenantId-yyyymm) to spread load. - Hierarchical partition keys (up to three levels, e.g.
/tenantId,/userId) to get past the 20 GB logical-partition limit for big tenants while still routing queries on a prefix. - The partition key of a container cannot be changed; migrating means copying to a new container (container copy jobs or the change feed).
Consistency levels, strongest to weakest: Strong, Bounded Staleness, Session (default), Consistent Prefix, Eventual. Session guarantees read-your-writes within a session token and suits most apps. Strong and Bounded Staleness reads cost twice the RUs of weaker levels, and Strong adds cross-region write latency. With multi-region writes, Strong is not available and concurrent updates are resolved by last-writer-wins on _ts (or a custom policy), so design for conflicts.
The change feed exposes ordered changes per partition key range, the basis for materialized views and event-driven pipelines.
Example
# pip install azure-cosmos azure-identity
from azure.cosmos import CosmosClient, PartitionKey
from azure.identity import DefaultAzureCredential
client = CosmosClient("https://cosmos-saas.documents.azure.com:443/", DefaultAzureCredential(),
consistency_level="Session")
db = client.get_database_client("saas")
# Hierarchical partition key: tenant, then user
events = db.create_container_if_not_exists(
id="events",
partition_key=PartitionKey(path=["/tenantId", "/userId"], kind="MultiHash"),
offer_throughput=4000)
events.upsert_item({"id": "e1", "tenantId": "t-big", "userId": "u-7", "type": "login"})
# Prefix query: routed only to t-big's partitions, not fanned out to all
rows = events.query_items("SELECT * FROM c WHERE c.type = 'login'", partition_key=["t-big"])
print(sum(1 for _ in rows)) # 1Remember
- Physical partitions cap at 10,000 RU/s and 50 GB; hot keys throttle early
- Partition keys are permanent; choose for even writes and query routing
- Hierarchical keys lift the 20 GB logical-partition ceiling
- Session consistency is the default; Strong costs more and blocks multi-region writes
Watch out for
- Choosing a low-cardinality key like /status or /country, creating a hot partition.