Azure Cosmos DB Partition Keys Explained
Azure Cosmos DB Partition Keys Explained
A Practical Guide for Architects and Developers
When developers first start using Azure Cosmos DB, the term partition key often sounds like just another database configuration.
It is not.
In Azure Cosmos DB, the partition key is one of the most important architectural decisions you will make. It directly influences:
Scalability
Query performance
Request Unit (RU) consumption
Data distribution
Transaction boundaries
Hot partitions
Storage limits
Cost
Future growth
A poorly chosen partition key may work perfectly when your application has 10,000 records and then become a serious performance problem when it grows to hundreds of millions of records.
The easiest way to understand partitioning is to think about one simple question:
Where should Cosmos DB place this piece of data, and how can it find it efficiently later?
That is essentially what the partition key helps Cosmos DB decide.
1. Why Does Cosmos DB Need Partitioning?
Imagine an e-commerce platform containing:
Orders Container
-------------------------
Order001
Order002
Order003
...
Order500,000,000
Initially, storing everything on one database server might work.
But what happens when the application grows to:
1 billion orders
100,000 requests per second
Customers across multiple countries
Multiple application regions
One machine cannot keep scaling forever.
Instead, Cosmos DB uses horizontal scaling.
Instead of making one server bigger:
DATABASE
|
Bigger Server
Cosmos DB spreads the workload:
Cosmos DB Container
|
----------------------------------
| | |
Partition A Partition B Partition C
The data is distributed across partitions.
This is where the partition key becomes important.
2. What Is a Partition Key?
Suppose an order looks like this:
{
"id": "ORD10001",
"customerId": "CUST101",
"product": "Laptop",
"amount": 1200
}
You could configure:
/customerId
as the partition key path.
Then:
Partition Key Path = /customerId
Partition Key Value = CUST101
Every document containing:
"customerId": "CUST101"
belongs to the same logical partition.
For example:
Customer CUST101
|
+-- Order001
+-- Order015
+-- Order034
+-- Order098
Another customer creates another logical partition:
Customer CUST102
|
+-- Order002
+-- Order021
If you have one million customer IDs, Cosmos DB can potentially create one million logical partitions.
Azure Cosmos DB defines logical partitions based on the partition-key value. Items that share the same value belong to the same logical partition. The combination of partition key and item ID uniquely identifies an item.
3. Container vs Logical Partition vs Physical Partition
This distinction is extremely important.
Container
A container is what developers normally work with.
For example:
Database: CommerceDB
Container: Orders
Inside that container are many logical partitions.
Orders
|
+-- CUST101
+-- CUST102
+-- CUST103
+-- CUST104
Logical Partition
A logical partition contains all records sharing the same partition-key value.
If:
Partition Key = /customerId
then:
CUST101 = Logical Partition 1
CUST102 = Logical Partition 2
CUST103 = Logical Partition 3
Conceptually:
Orders Container
|
+--- Logical Partition: CUST101
| Order1
| Order5
| Order9
|
+--- Logical Partition: CUST102
| Order2
| Order7
|
+--- Logical Partition: CUST103
Order3
Order4
Logical partitions are automatically created as data arrives. They also define an important transaction boundary: multi-item operations using Cosmos DB stored procedures or triggers must stay within one logical partition. The supplied Cosmos DB documentation also states a 20 GB storage limit per logical partition for regular partition keys.
4. What Is a Physical Partition?
Developers do not create physical partitions.
Cosmos DB manages them internally.
A physical partition is where data and throughput are physically distributed by the service.
Imagine:
Logical partitions
CUST101
CUST102
CUST103
CUST104
CUST105
CUST106
Cosmos DB may internally arrange them like:
Physical Partition 1
--------------------
CUST101
CUST103
Physical Partition 2
--------------------
CUST102
CUST105
Physical Partition 3
--------------------
CUST104
CUST106
You control:
Partition Key
Cosmos DB controls:
Physical Partitions
Replica placement
Partition splitting
Data distribution
The source explains that one or more logical partitions map to physical partitions and that Cosmos DB fully manages the physical layer. A physical partition can provide up to 10,000 RU/s and store up to 50 GB according to the supplied documentation.
5. How Cosmos DB Decides Where Data Goes
Cosmos DB uses hash-based partitioning.
Suppose:
customerId = CUST101
Internally, Cosmos DB conceptually performs something like:
Hash("CUST101")
|
V
7829384723
|
V
Partition Key Range
|
V
Physical Partition
You do not decide:
CUST101 → Physical Partition 2
Cosmos DB decides that automatically.
The process can be thought of as:
Document
|
V
Partition Key Value
|
V
Hash Function
|
V
Logical Partition
|
V
Physical Partition
Cosmos DB hashes partition-key values and distributes the resulting key space across physical partitions.
6. Why Partition-Key Choice Matters
Consider two architectures.
Architecture A
Partition Key = /country
Your users are:
USA 80%
India 10%
Canada 5%
Others 5%
Traffic becomes:
USA partition
████████████████████████████████████
India
█████
Canada
██
The USA logical partition receives most requests.
This can create a:
Hot Partition
Architecture B
Use:
/customerId
Now traffic could be spread across millions of customers.
CUST001 ██
CUST002 ██
CUST003 █
CUST004 ██
CUST005 █
...
Much better distribution.
A good partition key distributes both:
DATA
+
REQUESTS
not merely the number of documents.
The source specifically warns that uneven request distribution can concentrate requests on a small number of partitions, producing hot partitions, rate limiting, inefficient throughput usage, and higher cost.
7. Understanding RU Distribution
Suppose your Cosmos container has:
18,000 RU/s
and Cosmos DB currently uses three physical partitions.
Conceptually:
18,000 RU/s
|
V
---------------------------------
| | |
6,000 RU/s 6,000 RU/s 6,000 RU/s
Partition A Partition B Partition C
Now imagine almost all traffic goes to one logical partition located in Partition A:
Partition A needs 9,000 RU/s
Available = 6,000 RU/s
Meanwhile:
Partition B uses 500 RU/s
Partition C uses 400 RU/s
Your container theoretically has enough total capacity.
But Partition A is overloaded.
This is one reason good distribution matters.
The architectural objective is therefore not simply:
Choose a field containing many different values.
It is:
Choose a field that distributes storage and RU consumption while also supporting the application's main access patterns.
8. High Cardinality
One of the most important partition-key concepts is:
Cardinality
Cardinality means:
How many distinct values can this property contain?
Consider:
/status
Possible values:
New
Processing
Completed
Cancelled
Only four possibilities.
This is low cardinality.
Consider:
/customerId
Possible values:
CUST000001
CUST000002
CUST000003
...
CUST5000000
Millions of values.
This is high cardinality.
High cardinality normally gives Cosmos DB more opportunities to distribute data.
The documentation recommends partition keys that are stable, have high cardinality, and distribute both RU consumption and storage evenly.
9. Cardinality Alone Is Not Enough
This is where architects need to think differently from developers looking only at database structure.
Suppose you choose:
partitionKey = randomGuid
It has excellent cardinality:
74d82...
91adc...
4ff27...
...
Writes distribute beautifully.
But the application normally runs:
SELECT *
FROM c
WHERE c.customerId = "CUST101"
The partition key is not known.
Cosmos DB may have to search across partitions.
That becomes a:
Cross-Partition Query
So:
High Cardinality ≠ Automatically Good Partition Key
You need:
High Cardinality
+
Good Distribution
+
Query Alignment
+
Stable Value
The source explicitly identifies random high-cardinality keys that do not match application queries as an anti-pattern because the reads can still become cross-partition queries.
10. Point Reads: The Most Efficient Read Pattern
Suppose you know:
id = ORD1001
partitionKey = CUST101
Cosmos DB can route directly to the correct logical partition.
Conceptually:
GET
id = ORD1001
customerId = CUST101
|
V
CUST101
|
V
Correct Physical Partition
|
V
ORD1001
This is extremely efficient.
For example, in the .NET SDK:
ItemResponse<Order> response =
await container.ReadItemAsync<Order>(
"ORD1001",
new PartitionKey("CUST101"));
Notice that Cosmos receives both:
Item ID
+
Partition Key
That allows efficient routing.
11. Cross-Partition Queries
Now suppose the application executes:
SELECT *
FROM c
WHERE c.status = "Pending"
but your partition key is:
/customerId
Cosmos DB cannot identify one customer partition from the predicate.
Conceptually:
Query
|
----------------------------
| | |
Partition A Partition B Partition C
| | |
-------- Merge Results -----
|
V
Client
Cross-partition queries are not automatically bad.
Cosmos DB is designed to support them.
But at scale they may consume more RU and experience more latency than targeted queries.
For large read-heavy containers, the supplied document recommends considering a key that commonly appears in equality filters so queries can be routed efficiently.
12. The Architect's Partition-Key Formula
A useful mental formula is:
GOOD PARTITION KEY
=
High Cardinality
+
Even Data Distribution
+
Even RU Distribution
+
Query Alignment
+
Stable Value
+
Future Scalability
Ask six questions.
1. How many distinct values exist?
Bad:
/status
/country
/type
Better:
/customerId
/accountId
/tenantId
/deviceId
depending on the workload.
2. Can one value become extremely large?
Example:
TenantId = Microsoft
If that tenant produces enormous amounts of data, one logical partition can become problematic.
3. Will one value receive much more traffic?
Example:
CelebrityAccountId
could receive 50% of your traffic.
High cardinality across all accounts does not protect that individual logical partition.
4. Do queries normally know the partition key?
If yes:
Excellent
If no:
Expect cross-partition queries
5. Can this value ever change?
Avoid values such as:
status
currentRegion
department
processingState
Partition-key values are immutable after an item is created. Changing one effectively requires creating an item with the new partition key and deleting the original item.
6. What happens when the system becomes 100× larger?
Design for:
tomorrow's scale
not just today's data.
13. Common Partition-Key Anti-Patterns
Anti-Pattern 1: /status
{
"status": "Pending"
}
Possible values:
Pending
Approved
Rejected
Only three logical partition values.
If millions of requests use Pending, that partition can become very hot.
Anti-Pattern 2: /country
Imagine:
USA = 80 million records
Canada = 2 million
Mexico = 1 million
Distribution:
USA ██████████████████████████████
Canada ██
Mexico █
Very uneven.
The attached documentation warns specifically against low-cardinality values such as status, type, and country for larger workloads.
14. Is /id a Good Partition Key?
This answer is interesting:
Sometimes yes.
If:
Partition Key = /id
every item effectively receives its own logical-partition-key value.
Advantages:
Extremely high cardinality
Excellent write distribution
Good point-read behavior
Even storage distribution
For example:
ORD001 → PK ORD001
ORD002 → PK ORD002
ORD003 → PK ORD003
But suppose your main query is:
SELECT *
FROM c
WHERE c.customerId = "CUST101"
Cosmos cannot route it using /id.
It becomes a cross-partition query.
Therefore:
/id
can be excellent for:
Point-read-heavy systems
Write-heavy systems
but less attractive for workloads dominated by grouping or filtering using another business property.
The source makes exactly this distinction: /id gives excellent write distribution and efficient point reads, but queries filtering by unrelated fields can require cross-partition execution.
It also explains that item ID naturally provides many possible values and can therefore balance RU consumption and storage well.
15. Example: E-Commerce Order System
Consider:
{
"id": "ORD10001",
"customerId": "CUST501",
"storeId": "STORE22",
"status": "Pending",
"orderDate": "2026-08-13"
}
Possible partition keys:
/id
/customerId
/storeId
/status
/orderDate
Which is best?
There is no universal answer.
We need access patterns.
Suppose 80% of requests are:
Get customer's orders
Get customer order
Add customer order
Update customer order
Then:
/customerId
may be an excellent candidate.
Because:
Customer
|
+-- Order 1
+-- Order 2
+-- Order 3
Queries such as:
SELECT *
FROM c
WHERE c.customerId = "CUST501"
can target the relevant partition.
16. Multi-Tenant SaaS Example
Consider a SaaS platform:
{
"id": "POL10001",
"tenantId": "GEICO",
"policyId": "P10090"
}
You might choose:
/tenantId
This provides excellent tenant isolation conceptually:
Tenant A
Data...
Tenant B
Data...
Tenant C
Data...
Queries typically include:
tenantId
because users usually operate within their own tenant.
However, architects must ask:
Can one tenant grow enormously larger than every other tenant?
Suppose:
Tenant A = 50,000 policies
Tenant B = 100,000 policies
Tenant C = 300 million policies
Now pure:
/tenantId
may no longer be ideal.
This is where hierarchical or synthetic designs become valuable.
17. Synthetic Partition Keys
Sometimes one field does not satisfy all requirements.
You can create a synthetic value.
For example:
CustomerId + Year
Instead of:
CUST101
use:
CUST101_2025
CUST101_2026
CUST101_2027
Example document:
{
"id": "ORD1001",
"customerId": "CUST101",
"year": 2026,
"partitionKey": "CUST101_2026"
}
Now:
CUST101_2025
Orders...
CUST101_2026
Orders...
CUST101_2027
Orders...
This can help distribute a customer's growing data across multiple logical partitions.
But there is a tradeoff.
Query:
Give me ALL orders for CUST101
may now require multiple keys:
CUST101_2024
CUST101_2025
CUST101_2026
Architecture is always about tradeoffs.
The source categorizes synthetic keys as useful when no single field provides both sufficient distribution and alignment with workload patterns.
18. Hierarchical Partition Keys
Azure Cosmos DB also supports hierarchical partition keys.
Instead of only:
TenantId
you can design a hierarchy such as:
TenantId
|
+-- CustomerId
|
+-- OrderId
For example:
/tenantId
/customerId
/orderId
This is particularly useful for large multi-tenant architectures.
Consider:
Tenant: InsuranceCompanyA
|
+--- Customer 100
| Policy 1
| Policy 2
|
+--- Customer 200
Policy 3
Policy 4
The source notes that hierarchical partition keys can use up to three levels and can help when a single regular partition-key value could otherwise grow beyond the standard logical-partition storage boundary.
19. Regular vs Synthetic vs Hierarchical Partition Keys
A useful architect comparison is:
| Strategy | Example | Best Use |
|---|---|---|
| Regular | /customerId | Straightforward workloads |
| Synthetic | customerId_year | Better distribution when one property is insufficient |
| Hierarchical | tenantId/customerId/orderId | Large multi-level workloads |
/id | Unique item ID | Point-read/write-heavy systems |
The supplied documentation additionally discusses Global Secondary Indexes as a preview option for workloads needing different partitioning strategies for independent query patterns.
20. Global Secondary Indexes
A classic Cosmos DB architecture problem is:
Writes happen by CustomerId
but users search by:
OrderId
ProductId
Region
StoreId
One partition key cannot necessarily optimize every access pattern.
Conceptually, a secondary representation might look like:
Primary Container
Partition Key
/customerId
|
V
Secondary Index Representation
Partition Key
/orderId
This allows different query patterns to have different partition strategies.
The supplied Cosmos DB material describes Global Secondary Indexes as additional synchronized containers with alternative partition keys designed to reduce cross-partition queries for independent access patterns. It identifies the feature as preview in the provided source.
For production architecture, preview-feature suitability should always be evaluated against your organization's production-support requirements.
21. Partition Key and Transactions
Another major architectural consequence is transaction scope.
Suppose:
Partition Key = /customerId
and Customer CUST101 has:
Order
Payment
Invoice
If the related items live within the same logical partition, some multi-item transactional operations can be performed within that partition.
Conceptually:
Logical Partition CUST101
+ Order
+ Invoice
+ Payment
But:
CUST101
and:
CUST102
are different logical partitions.
Cosmos DB stored procedures and triggers are scoped to one logical partition.
For architecture design, this means partitioning is not merely a performance concern.
It can influence your:
transaction boundary
aggregate boundary
data model
22. Partition Key and Domain-Driven Design
This leads to an interesting architectural idea.
In Domain-Driven Design we often think about:
Aggregate Root
For example:
Customer
|
+ Orders
+ Addresses
+ Preferences
If operations normally happen within one customer boundary, then:
customerId
may also provide a natural partition boundary.
So good Cosmos modeling often asks:
What data belongs together?
What data is queried together?
What data is updated together?
instead of asking:
What would my SQL table look like?
Cosmos DB modeling should follow access patterns and scale boundaries, not simply normalized relational-table design.
23. Partition Key Is Immutable
This is a very important developer consideration.
Suppose you choose:
/status
as the partition key.
Document:
{
"id": "100",
"status": "Pending"
}
Later:
Pending → Completed
But status is your partition key.
Changing it would effectively mean moving the item from:
Pending Logical Partition
to:
Completed Logical Partition
Cosmos DB does not update a partition-key value in place.
Instead, conceptually:
Create new document
+
Delete old document
would be required.
The attached material therefore recommends choosing a partition-key value that does not change.
24. Can I Change the Container's Partition Key Later?
Not directly in place.
That makes partition-key design a major architecture decision.
If your original container uses:
/customerId
and later you want:
/tenantId
the supplied documentation says the data must be moved to another container with the desired partition strategy; it also references container-copy capabilities for that migration scenario.
Therefore partition-key selection should be part of your architecture review, not an implementation detail chosen casually during coding.
25. Developer Example in C#
Container creation:
Database database =
await cosmosClient.CreateDatabaseIfNotExistsAsync(
"CommerceDB");
Container container =
await database.CreateContainerIfNotExistsAsync(
id: "Orders",
partitionKeyPath: "/customerId");
Create an order:
var order = new
{
id = "ORD10001",
customerId = "CUST101",
product = "Laptop",
amount = 1200
};
await container.CreateItemAsync(
order,
new PartitionKey(order.customerId));
Read it:
var response =
await container.ReadItemAsync<Order>(
"ORD10001",
new PartitionKey("CUST101"));
Conceptually Cosmos DB now knows:
ID = ORD10001
PartitionKey = CUST101
which allows efficient point routing.
26. A Useful Design Exercise
Before creating a Cosmos container, architects should write down the primary access patterns.
Example:
Orders Container
1. Get order using OrderId
2. Get customer's orders
3. Create order
4. Update customer order
5. Get pending orders
6. Search orders by date
Then estimate frequency.
Get customer orders 45%
Get order 25%
Create order 15%
Update order 10%
Pending-order search 3%
Date reporting 2%
Now evaluate candidates.
| Candidate | Distribution | Query Alignment | Risk |
|---|---|---|---|
| customerId | Excellent | Excellent | Large customer |
| status | Poor | Limited | Hot partition |
| country | Poor | Poor | Uneven distribution |
| orderId | Excellent | Good point reads | Customer queries cross partitions |
| customerId + year | Excellent | Good | More query complexity |
Now architecture becomes evidence-based instead of guesswork.
27. Questions an Architect Should Ask
Before approving a partition strategy, ask:
Data
How many records today?
How many records in three years?
How large can one partition-key value become?
Traffic
How many reads per second?
How many writes per second?
Can one customer generate much more traffic?
Queries
What are the top five queries?
Do they include the partition key?
Are most reads point reads?
Transactions
Which items need transactional operations together?
Multi-Tenancy
Can one tenant become disproportionately large?
Growth
Will the architecture still work at 10× or 100× traffic?
These questions matter more than simply asking:
Which property looks unique?
28. A Simple Decision Tree
Use this mental model:
START
|
|-- What property appears in most queries?
|
V
Candidate Key
|
|-- Does it have many distinct values?
| |
| NO
| |
| -> Consider another key,
| synthetic key,
| or hierarchical key
|
YES
|
|-- Can one value become extremely large?
| |
| YES
| |
| -> Consider additional partitioning
|
NO
|
|-- Can one value receive huge traffic?
| |
| YES
| |
| -> Hot-partition risk
|
NO
|
|-- Is the value stable?
| |
| NO
| |
| -> Reject candidate
|
YES
|
|-- Does it align with main reads/writes?
|
YES
|
-> Strong Partition-Key Candidate
29. The Biggest Partition-Key Mistake
A common mistake is designing like this:
Database first
Application second
For Cosmos DB, reverse the thinking:
Application Access Patterns
|
V
Domain Boundaries
|
V
Scale Requirements
|
V
Partition Strategy
|
V
Container Design
The right question is not:
Which field should I use as my partition key?
The better question is:
How will my application access and scale this data over its lifetime?
30. Final Architectural Mental Model
Think about Cosmos DB using this picture:
APPLICATION REQUEST
|
V
PARTITION KEY
|
V
HASH
|
V
LOGICAL PARTITION
|
V
PHYSICAL PARTITION
|
V
REPLICA SET
|
V
DATA
Logical partitions group records by partition-key value.
Physical partitions provide the infrastructure needed to store and process those logical partitions.
Replica sets provide durability, availability, and consistency. The supplied Cosmos documentation explains that each physical partition consists of multiple replicas managed by Cosmos DB.
As developers and architects, we generally should not attempt to manage physical partitions.
We should concentrate on the decision we can control:
The Partition Key.
Key Takeaways
Remember these rules:
1. Partition key is an architecture decision, not just a database field.
2. Logical partitions are created from partition-key values.
/customerId
CUST101 → Logical Partition
CUST102 → Logical Partition
3. Cosmos DB manages physical partitions automatically.
4. Favor high-cardinality keys, but do not stop there.
You also need:
even traffic
+
even storage
+
query alignment
5. Avoid low-cardinality values for large workloads.
Examples:
/status
/type
/country
6. Design around your most important access patterns.
7. Point reads using ID + partition key are highly efficient.
8. Cross-partition queries are supported, but they should be understood and intentionally designed for.
9. Watch for hot partitions.
10. Partition-key values are immutable.
11. Consider synthetic keys when one property is insufficient.
12. Consider hierarchical partition keys for large hierarchical or multi-tenant workloads.
13. /id can be excellent for point-read/write workloads but poor for unrelated filtering patterns.
And perhaps the most useful rule for an architect:
Don't choose the partition key based only on how the data looks today. Choose it based on how the application will read, write, transact, and scale tomorrow.
Award-Winning Principal Architect | .Net | Full Stack| GenAI |Azure| Guidewire | Driving Innovation with AI & Cloud | SMIEEE | Cybersecurity & TOGAF Expert | Author | PhD
Comments
Post a Comment