DynamoDB Partition Key Design: Why Hot Partitions Kill Performance and How to Fix Them
You built a DynamoDB table, shipped it to production, and now reads are throttling under load — even though your provisioned capacity looks fine on paper. Nine times out of ten, the culprit is a poorly chosen partition key that funnels all traffic into a single physical partition. Understanding how DynamoDB partition keys work internally is the difference between a table that scales effortlessly and one that becomes a production incident at 2am.
TL;DR: DynamoDB Partition Key Performance at a Glance
| Concern | Key Point |
|---|---|
| What is a partition key? | The attribute DynamoDB hashes to determine which physical storage node holds the item |
| What is a hot partition? | A single partition receiving disproportionate read/write traffic, exhausting its per-partition throughput ceiling |
| Why low-cardinality keys fail | All items with the same key value land on the same partition — traffic concentrates regardless of total table capacity |
| Primary fix | Choose a high-cardinality attribute as the partition key, or composite a low-cardinality key with a suffix/UUID |
| Detection tool | CloudWatch Contributor Insights for DynamoDB |
How DynamoDB Partitioning Works Under the Hood
DynamoDB is a distributed key-value store. When you write an item, DynamoDB runs the partition key value through an internal hash function and maps the result to one of N physical partitions. Each partition is an independent storage node with its own throughput ceiling — currently documented as up to 3,000 RCUs and 1,000 WCUs per partition. The table-level capacity you provision is spread across all partitions, but traffic is only served from the partition that owns a given key range.
This is the core tension: you can provision 10,000 WCUs on a table, but if every write targets the same partition key value, all 10,000 WCUs are irrelevant. That single partition is capped at its own limit, and every request beyond that cap is throttled — even if the rest of the table is completely idle.
(partition key value)"] PA["Partition A
🔥 HOT — throttling"] PB["Partition B
idle"] PC["Partition C
idle"] Throttle["ProvisionedThroughputExceededException"] Client --> Hash Hash -->|"pk = 'ACTIVE' (90% of traffic)"| PA Hash -->|"pk = 'PENDING' (8%)"| PB Hash -->|"pk = 'CLOSED' (2%)"| PC PA -->|"exceeds per-partition ceiling"| Throttle style PA fill:#ff4d4d,color:#fff style Throttle fill:#cc0000,color:#fff style PB fill:#90ee90 style PC fill:#90ee90
- Hash function: DynamoDB hashes the partition key value to determine placement. Identical key values always hash to the same partition.
- Partition ceiling: Each physical partition has an independent throughput limit. Table-level capacity does not override per-partition limits.
- Idle capacity: Partitions B and C sit underutilized while Partition A is throttled — provisioned capacity is wasted.
- Throttling: Requests to the hot partition return
ProvisionedThroughputExceededExceptionregardless of table-level headroom.
What a Hot Partition Looks Like in Practice
The symptom that usually triggers the investigation: ProvisionedThroughputExceededException errors in your application logs, but CloudWatch shows your table-level consumed capacity is well below provisioned. You add more capacity — the errors persist. That mismatch is the tell.
A real example: a multi-tenant SaaS application used status as the partition key, with values like PENDING, ACTIVE, CLOSED. The overwhelming majority of writes targeted ACTIVE. The table had three effective partitions, and one absorbed nearly all write traffic. Adding WCUs to the table did nothing because the hot partition's ceiling was the actual bottleneck — not the table's aggregate capacity.
Think of provisioned capacity like lanes on a highway. Adding lanes helps when traffic is distributed. If every car insists on using lane 1, you can add 50 lanes and still have gridlock in lane 1.
Diagnosing a Hot Partition with Contributor Insights
Before redesigning your key schema, confirm which partition key values are actually driving the throttling. Guessing wastes time. CloudWatch Contributor Insights for DynamoDB surfaces the top contributors to throttled requests and consumed capacity, broken down by partition key value.
Enable Contributor Insights on your table:
aws dynamodb update-contributor-insights \
--table-name YourTableName \
--contributor-insights-action ENABLE
Verify it is active:
aws dynamodb describe-contributor-insights \
--table-name YourTableName
Once enabled, navigate to CloudWatch → Contributor Insights and look for the DynamoDB rule scoped to your table. The 'Top N Contributors' view will show which partition key values are responsible for the highest throttled request counts. If one or two values dominate the list, you have confirmed a hot partition — not a capacity sizing problem.
Also pull the consumed capacity metrics per partition key to see the distribution:
aws cloudwatch get-metric-statistics \
--namespace AWS/DynamoDB \
--metric-name ConsumedWriteCapacityUnits \
--dimensions Name=TableName,Value=YourTableName \
--start-time 2024-01-01T00:00:00Z \
--end-time 2024-01-01T01:00:00Z \
--period 300 \
--statistics Sum
This gives you table-level consumed WCUs over time. Cross-reference with Contributor Insights to identify whether the consumption is concentrated on specific key values.
Choosing a Better Partition Key: The Core Principles
The goal is uniform distribution of requests across as many partitions as possible. DynamoDB cannot rebalance traffic for you — the key design is the only lever you control.
Principle 1: Maximize Cardinality
A partition key with high cardinality means many distinct values, which means many partitions, which means traffic spreads naturally. User IDs, order IDs, device IDs, UUIDs — these are good candidates. Status codes, boolean flags, country codes, day-of-week — these are traps. The fewer distinct values, the more concentrated the traffic.
Principle 2: Distribute Access Patterns, Not Just Values
Cardinality alone is not enough. If your access pattern always reads the same 10 user IDs out of 10 million, those 10 partitions are still hot. The key needs to distribute your actual traffic, not just your data. Analyze your read/write patterns before finalizing a key design — not after.
Principle 3: Write Sharding for Unavoidably Low-Cardinality Keys
Sometimes the natural access pattern requires a low-cardinality key — for example, a time-series table where the query pattern is 'give me all events for today.' In these cases, you can artificially increase cardinality by appending a random suffix to the partition key at write time.
Instead of writing with pk = "2024-01-15", write with pk = "2024-01-15#3" where the suffix is a random integer between 0 and N-1. At read time, you issue N parallel queries — one per shard suffix — and merge the results in your application. This is called write sharding.
N = random(0..4)"] P0["Partition: date#0"] P1["Partition: date#1"] P2["Partition: date#2"] P3["Partition: date#3"] P4["Partition: date#4"] Read(["Application Read"]) Fan["Fan-out: 5 parallel queries
(one per shard suffix)"] Merge["Merge results
in application"] Write --> Suffix Suffix --> P0 Suffix --> P1 Suffix --> P2 Suffix --> P3 Suffix --> P4 Read --> Fan Fan -->|"query date#0"| P0 Fan -->|"query date#1"| P1 Fan -->|"query date#2"| P2 Fan -->|"query date#3"| P3 Fan -->|"query date#4"| P4 P0 --> Merge P1 --> Merge P2 --> Merge P3 --> Merge P4 --> Merge style P0 fill:#4da6ff style P1 fill:#4da6ff style P2 fill:#4da6ff style P3 fill:#4da6ff style P4 fill:#4da6ff
- Write path: The application appends a random shard suffix (0 to N-1) to the base key before writing. Each write lands on a different partition.
- Read path: To retrieve all items for a base key, the application fans out N parallel queries — one per shard suffix — and merges results.
- Trade-off: Write sharding eliminates hot partitions at the cost of read-time scatter-gather complexity. Choose N based on your expected write throughput per key value.
Partition Key Patterns: What Works and What Doesn't
| Pattern | Example | Verdict | Why |
|---|---|---|---|
| UUID / high-cardinality ID | userId = "uuid-v4" | ✅ Good | Naturally distributes across many partitions |
| Composite key with sort key | pk = tenantId, sk = timestamp | ✅ Good | Separates access scope from sort order; works if tenantId traffic is balanced |
| Write sharding | pk = "date#N" | ✅ Good (with trade-off) | Distributes writes; requires scatter-gather reads |
| Status / enum field | pk = "ACTIVE" | ❌ Bad | Low cardinality; dominant value creates hot partition |
| Date string | pk = "2024-01-15" | ❌ Bad | All writes for a given day hit one partition; today is always hot |
| Boolean flag | pk = true/false | ❌ Bad | Only two possible partitions; guaranteed concentration |
| Monotonically increasing ID | pk = autoincrement | ⚠️ Risky | Sequential writes cluster on the latest partition; similar to hot partition behavior |
IAM Permissions for Contributor Insights and CloudWatch Access
Enabling Contributor Insights and reading CloudWatch metrics requires explicit IAM permissions. The following policy covers the minimum required actions for the diagnostic workflow described above.
🔽 Click to expand — IAM policy for Contributor Insights and CloudWatch diagnostics
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DynamoDBContributorInsights",
"Effect": "Allow",
"Action": [
"dynamodb:UpdateContributorInsights",
"dynamodb:DescribeContributorInsights",
"dynamodb:ListContributorInsights"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/YourTableName"
},
{
"Sid": "CloudWatchReadMetrics",
"Effect": "Allow",
"Action": [
"cloudwatch:GetMetricStatistics",
"cloudwatch:ListMetrics"
],
"Resource": "*"
}
]
}
Note: cloudwatch:GetMetricStatistics and cloudwatch:ListMetrics do not support resource-level restrictions — "Resource": "*" is required for these actions, as documented in the CloudWatch Service Authorization Reference.
The Misdiagnosis That Wastes Hours
The most common wrong turn: seeing throttling errors and immediately increasing provisioned capacity or switching to on-demand mode. On-demand mode helps with unpredictable aggregate traffic spikes, but it does not eliminate per-partition throughput limits. A hot partition on an on-demand table still throttles — the table just adapts its aggregate capacity, not the per-partition ceiling.
The symptom that confirms you went down the wrong path: you switched to on-demand, throttling dropped briefly, then crept back as traffic grew. The partition key distribution is the root cause. On-demand mode is not a substitute for correct key design.
This is the non-obvious interaction: on-demand capacity mode and per-partition limits are independent axes. Changing one does not affect the other.
Redesigning an Existing Table's Partition Key
DynamoDB does not allow you to modify a table's primary key schema after creation. If your existing table has a fundamentally broken partition key, your options are:
- Create a new table with the correct key schema and migrate data using AWS Data Pipeline, AWS Glue, or a custom scan-and-write job.
- Use a Global Secondary Index (GSI) with a better partition key to serve the high-traffic access pattern, while keeping the original table schema intact. GSIs have their own partition key and their own throughput — a GSI with a high-cardinality partition key distributes reads independently of the base table.
- Add a write-sharding suffix at the application layer for new writes, and backfill existing items. This requires application-side changes to both write and read paths.
pk: status (low cardinality)"] GSI["Global Secondary Index
pk: userId (high cardinality)"] HotP["Hot Partition
🔥 status = ACTIVE"] DistP["Distributed Partitions
✅ by userId"] App -->|"high-traffic reads"| GSI App -->|"writes"| BaseTable BaseTable --> HotP BaseTable -->|"GSI projection"| GSI GSI --> DistP style HotP fill:#ff4d4d,color:#fff style DistP fill:#90ee90 style GSI fill:#ffd700
- GSI approach: The base table retains its original key. A GSI projects a subset of attributes with a high-cardinality partition key. High-traffic reads are redirected to the GSI, distributing load across its own partition space.
- GSI throughput: GSI capacity is independent of the base table. Provision or configure the GSI separately based on its own read traffic.
- Write amplification: Every write to the base table that affects projected GSI attributes also writes to the GSI. Factor this into WCU planning.
Wrap-Up: DynamoDB Partition Key Design Is a First-Class Decision
Hot partitions in DynamoDB are almost always a design problem, not a capacity problem. The partition key determines how traffic distributes across physical storage nodes, and no amount of provisioned capacity compensates for a key that concentrates all requests on one node. Choose a high-cardinality key that reflects your actual access distribution, use write sharding when the natural key is unavoidably low-cardinality, and verify your assumptions with Contributor Insights before and after any change.
For further reading, the AWS documentation on best practices for designing and using partition keys effectively covers additional patterns including time-series design and GSI overloading.
Glossary: Key Terms
| Term | Definition |
|---|---|
| Partition Key | The primary attribute DynamoDB hashes to determine which physical partition stores an item. Also called the hash key. |
| Hot Partition | A physical partition receiving a disproportionate share of read or write requests, causing throttling even when table-level capacity is available. |
| Cardinality | The number of distinct values an attribute can hold. High cardinality means more distinct values and better traffic distribution. |
| Write Sharding | A technique that appends a random suffix to a low-cardinality partition key to artificially distribute writes across multiple partitions. |
| Contributor Insights | A CloudWatch feature that identifies the top contributors to DynamoDB consumed capacity and throttled requests, broken down by partition key value. |
| Global Secondary Index (GSI) | An index with a partition key and optional sort key different from the base table, enabling alternative access patterns with independent throughput. |
Comments
Post a Comment