Why Hot Partitions Happen
A hot partition occurs when a disproportionate amount of traffic — reads or writes — targets a single Partition Key. DynamoDB's per-partition limits are hard ceilings: 3,000 RCUs and 1,000 WCUs per second per partition. Exceed them and DynamoDB throttles requests to that partition, returning ProvisionedThroughputExceededException errors.
Common causes:
- Sequential IDs: If your PK is an auto-incrementing integer, all new writes go to the "highest" partition, creating a perpetual hot spot.
- Popular entities: A celebrity's profile page receiving millions of reads per minute.
- Aggregation keys: Storing all votes, events, or logs under a single key (the "Recent Posts" anti-pattern).
- Time-series data: Using PK = "LOGS#TODAY" concentrates all writes for the day on one partition.
DynamoDB Adaptive Capacity (enabled by default) can temporarily burst a partition beyond its baseline allocation by borrowing unused capacity from other partitions. This helps with occasional spikes but cannot solve sustained hot partitions. Do not design around adaptive capacity — it is a safety net, not a solution.
Solution: Write Sharding
Write sharding distributes traffic across multiple logical partitions that represent the same conceptual entity.
The pattern:
# Instead of writing to:
PK: "LEADERBOARD#GLOBAL"
# Write to one of N shards:
PK: "LEADERBOARD#GLOBAL#<random(1-10)>"
Writing (application code):
import random
shard_count = 10
shard_id = random.randint(1, shard_count)
partition_key = f"LEADERBOARD#GLOBAL#{shard_id}"
table.put_item(Item={
"PK": partition_key,
"SK": f"SCORE#{user_id}",
"Score": score,
"UserId": user_id
})
Reading (scatter-gather):
import concurrent.futures
def query_shard(shard_id):
return table.query(
KeyConditionExpression=Key("PK").eq(f"LEADERBOARD#GLOBAL#{shard_id}"),
ScanIndexForward=False, # descending order
Limit=10
)["Items"]
with concurrent.futures.ThreadPoolExecutor() as executor:
results = list(executor.map(query_shard, range(1, 11)))
# Merge and sort all shard results in memory
all_scores = [item for shard in results for item in shard]
top_10 = sorted(all_scores, key=lambda x: x["Score"], reverse=True)[:10]
The trade-off: Write sharding makes writes cheap and scalable but makes reads more expensive — you must query all shards in parallel and aggregate in memory. Choose the shard count based on your write throughput requirement: if you need 5,000 WCUs on one logical key, use at least 5 shards (each handling ~1,000 WCUs).
Choosing shard count: Too few shards and you still get throttling. Too many shards and your scatter-gather reads become expensive. A common starting point is ceil(expected_peak_WCUs / 800) — using 800 instead of 1,000 as a safety margin.