Designing for Hot Keys: When Distributed Systems Stop Distributing
Learn how to mitigate hot keys in distributed systems using bucketed writes, caching, replication, queues, and admission control.
Distributed systems usually spread data by hashing keys across nodes so no one node carries all the load. A hot key breaks that assumption. It is a key whose access volume greatly exceeds the volume of other keys during a given period. A global view counter or a popular item identifier can receive millions of reads or writes each second. Every request targets the same partition, even when the rest of the cluster has spare capacity.
The result is uneven load. One node reaches its CPU, network, thread, or connection limits while other nodes remain underused. Adding nodes does not solve the problem because traffic continues to resolve to the same partition. The overloaded shard becomes the bottleneck for a system that was intended to distribute work.
Hot keys also create failure risks. If a popular cache entry expires or its host fails, many requests can fall through to the backing data store at once. This cache breakdown can overwhelm the database and trigger broader failures. Teams should monitor access patterns and alert when one key represents an unusual share of traffic, latency, or bandwidth.
Split a Logical Key Into Physical Buckets
A common mitigation is to split one logical key into several physical buckets. Instead of updating a single counter, a writer chooses a bucket and updates that bucket. The combined value is calculated by reading and aggregating all buckets.
public void increment(String key) {
int bucket = ThreadLocalRandom.current().nextInt(NUM_BUCKETS);
jedis.incr(key + ":" + bucket);
}
public int readTotal(String key) {
int total = 0;
for (int i = 0; i < NUM_BUCKETS; i++) {
total += Integer.parseInt(jedis.get(key + ":" + i));
}
return total;
}
This pattern spreads writes across hash slots and nodes. The tradeoff is more work during reads. In Redis Cluster, a native `MGET`cannot read keys from different hash slots in one command. The client must issue individual reads, use parallel reads, or provide a scatter-gather layer that collects the bucket values. The aggregated total is also not an atomic snapshot because each bucket can be read at a different instant.
Bucketed counters work well when approximate totals are acceptable, such as views, metrics, or telemetry. They are less appropriate when every read requires a strictly current, exact value. The number of buckets should reflect peak write volume, expected read frequency, and the cost of aggregation.
Serve Read-Heavy Hot Keys From Local Memory
When a hot key is primarily read, an application cache can reduce pressure on the shared store. A short-lived in-memory cache lets many requests reuse a recently fetched value before another request reaches Redis or a database.
private static final LoadingCache<String, String> HOT_KEY_CACHE =
Caffeine.newBuilder()
.expireAfterWrite(50, TimeUnit.MILLISECONDS)
.build(key -> {
try (Jedis jedis = JEDIS_POOL.getResource()) {
return jedis.get(key);
}
});
With this setup, most reads can be served from memory, and cache misses fetch the current value from Redis. The short expiration bounds the staleness window. This is useful when the business can tolerate a briefly outdated response in exchange for much lower read pressure on the shared data store.
Systems can also use layered caches. A content delivery network or edge cache can absorb public traffic, followed by an application cache, then a distributed cache, and finally the durable store. Each layer should have a clear expiration policy and a plan for cache invalidation. The goal is to keep repeated reads from reaching the most constrained shared component.
Replicate Values When Reads Are More Important Than Writes
Another option for a read-dominated hot key is replication. The system stores several copies of the logical value on different shards, then distributes reads across the copies.
int idx = ThreadLocalRandom.current().nextInt(REPLICA_COUNT);
String replicaKey = originalKey + ":" + idx;
String value = jedis.get(replicaKey);
Replication can increase read capacity because different nodes serve independent requests. It adds work to writes because every replica must be updated. If an update fails for one replica, callers can see different values depending on the chosen key. This approach is useful when stale or temporarily inconsistent reads are acceptable. It needs an explicit repair strategy for failed replica updates.
Smooth Write Spikes With Queues and Admission Control
A hot key with a high write rate may need a different approach. Instead of applying every update immediately, the service can publish updates to a queue and let workers batch or aggregate them before writing to the shared store. This smooths spikes and protects the storage layer, but it adds write latency and creates a delay before the final value is visible.
Rate limiting and backpressure can protect the system when a sudden surge would otherwise overwhelm one partition. These controls should preserve important traffic first and reject or delay work that can safely wait. The correct policy depends on whether the key represents a counter, inventory, a payment state, or another domain with different consistency requirements.
Choose the Tradeoff Explicitly
Every hot key mitigation trades one property for another. Buckets increase read work and make aggregate snapshots non-atomic. Application caching introduces staleness. Replication increases write cost and requires reconciliation. Queues delay visibility. These tradeoffs are often preferable to letting one shard become a single point of congestion.
The right design starts with the semantics of the key. Decide whether the value must be exact, how long it can be stale, whether writes can be delayed, and what happens when the shared store is unavailable. Then measure the key’s read rate, write rate, payload size, and partition utilization. A mitigation that is safe for a view counter can be incorrect for inventory or payment state.
Hot keys are a normal condition in systems with uneven demand. Monitoring should detect them early, and the architecture should provide a way to redistribute load before one partition limits the entire service. With bucketed writes, local caching, replication, queues, and admission control, teams can turn a concentrated source of traffic into a managed capacity decision instead of a cluster-wide incident.
Opinions expressed by DZone contributors are their own.