Designing a Distributed Cache Service: Redis Cluster Internals and Hot Keys
Every architecture diagram has a box labeled "cache," and most engineers' knowledge of the box stops at GET/SET/TTL. Design the box itself — a managed, multi-tenant, distributed cache service — and you inherit a concentrated dose of distributed systems: sharding, failover, memory management under adversarial workloads, and the one problem sharding fundamentally cannot fix.
Requirements
Sub-millisecond p99 GET/SET, clusters from 1GB to 10TB, scale out/in without downtime, survive node loss with bounded data loss (it's a cache — losing data is acceptable, losing the service is not), multi-tenant isolation.
Sharding: hash slots, not consistent-hash rings
Redis Cluster's design choice is worth copying and worth explaining: the keyspace maps to 16,384 fixed slots (CRC16(key) mod 16384), and slots are assigned to nodes in contiguous ranges. Compare with a raw consistent-hash ring:
- Rebalancing = migrate whole slots — a bounded, enumerable unit with a live-migration protocol (keys stream to the new owner; requests for mid-migration keys get redirected with
ASK). - Cluster topology is a small table (
slot → node) that clients cache — a smart client goes straight to the right node, no proxy hop; on topology change it gets aMOVEDredirect and refreshes. - Virtual-node balancing comes for free — slots are the virtual nodes.
The general lesson: fixed logical partitions + a partition-to-node map beats hashing-to-nodes directly, because it turns rebalancing from "rehash the world" into "move slot 7,204." (Kafka partitions, Dynamo vnodes, and every shard-map service are the same idea.)
Replication and failover: what a cache may cheaply promise
Each slot range gets a primary + replica(s) with asynchronous replication — a failover can lose the last few milliseconds of writes, and for a cache that is the right trade (the source of truth is the database behind it; a lost cache write is a future cache miss, not lost data — say that sentence in the interview). Failure detection and promotion run on a gossip protocol: nodes ping each other, a majority marking a primary as failed triggers replica promotion. The subtle requirement is the majority — without it, a partitioned minority promotes its own primaries and you get split-brain: two primaries for one slot range serving diverging data. A cache tolerates brief divergence, but tenants observing rollback of their own writes file tickets; the quorum rule is what bounds the mess.
Memory: eviction is the product
A cache at capacity is always deciding what to forget; eviction policy is user-visible behavior, not an implementation detail.
- LRU is approximated, not exact: a true doubly-linked LRU list costs 2 pointers per key (24+ bytes of overhead per tiny key) and cross-thread contention on every touch. Redis samples K random keys and evicts the least-recent among them — 90% of the benefit, none of the bookkeeping. LFU variants resist scans (one table dump shouldn't flush your working set).
- TTL expiry is lazy + probabilistic: keys are checked on access, plus a background sampler; a billion keys cannot all carry precise timers.
- Fragmentation is the silent killer: allocator behavior under churning value sizes can leave 30% of RAM unusable; slab-class allocation (memcached) or active defrag (Redis) is the counter.
Multi-tenancy makes eviction political: per-tenant memory quotas with per-tenant eviction, or one noisy tenant's churn evicts everyone else's working set. Isolation of eviction pressure is as important as isolation of CPU.
Hot keys: the problem sharding cannot solve
Sharding distributes keys, not popularity. One viral post's counter, one flash-sale product's stock, one celebrity's profile — a single key doing 500K QPS lands on one primary, and adding nodes changes nothing: the ceiling is one machine's capacity for one key. This is the distributed-cache version of the hot-partition problem, and the mitigation ladder is a great interview sequence because each rung has a cost:
1. Detect: per-node top-K sketch (count-min) -> flag keys > threshold
2. Client-side micro-cache: hot keys cached in the app process for 1-5s
(staleness budget bought for a 100-1000x load reduction)
3. Key replication: hot key copied to R nodes; reads pick a random replica
(writes now fan out -> only for read-hot, write-rare keys)
4. Key splitting: counter:{k} -> counter:{k}:0..9, read sums shards
(write-hot keys; reads pay the aggregation)
Note rung 2 is the highest-leverage and the most commonly forgotten: the best distributed-cache design includes the caller's memory as tier zero.
The evil twin — hot slot after a poor hash or a user:{id}:* multi-key pattern forcing co-location (hash tags) — is solved by re-slotting, which is why the slot map being movable (design decision #1) matters.
Failure modes that define the SLO
Thundering herd / cache stampede: popular key expires; 10K requests miss simultaneously and hit the database. Fixes: request coalescing (one fetch, others wait), stale-while-revalidate, TTL jitter so co-created keys don't co-expire. Cold start: a rebooted 500GB node serving 0% hit ratio can knock over the database it protects — warm from a replica or snapshot before taking traffic, and cap miss-path concurrency (the cache should shed rather than forward a stampede). A cache service's real SLO is arguably "maximum miss rate delivered to the origin," not its own latency.
| Interview probe | Answer sketch |
|---|---|
| Why not strong consistency? | Doubles latency and halves availability to protect data whose truth lives elsewhere |
| Resize from 3 to 5 nodes live? | Slot migration with ASK/MOVED redirects; keys move slot-by-slot, no global pause |
| One key at 1M QPS? | Client micro-cache + replicate-for-reads or split-for-writes; sharding alone can't help |
| Cache vs database inconsistency? | TTL as backstop, CDC-driven invalidation as optimization, never write-back for data you can't lose |
The through-line: a cache buys performance by promising less — async replication, approximate LRU, acceptable loss. The craft is in choosing precisely which promises to break and making the breakage boring.
Keep reading
Designing Chat Presence: Online, Away, and the Lie of Instant Status
Heartbeat vs subscriptions, fan-out of presence, privacy, and why a boolean online flag does not survive a million concurrent sockets.
Consistency Models Beyond CAP: Linearizability, Causal, and Session Guarantees
'Strong vs eventual' is a cartoon. The real spectrum — linearizable, sequential, causal, session guarantees — and how to pick per operation, not per system.
Designing an API Gateway: The Front Door as a System
Authentication, routing, rate limits, transformations, and a plugin chain — in a tier that must add ~1ms and never be the outage. Envoy/Kong architecture from scratch.
Designing a CDC Pipeline: Change Data Capture from Binlog to Downstream
Dual writes are a lie; the database's own log is the truth. Log-based CDC, the snapshot-plus-stream handoff, schema evolution, and ordering guarantees that survive resharding.
Designing a Content Moderation Pipeline: Trust and Safety at Platform Scale
A billion uploads a day, a legal requirement to act in hours, and irreversible mistakes in both directions: hash matching, classifier tiers, human review queues, and appeals.
Designing a Distributed Rate Limiter: Local Buckets, Global Truth
One user, twenty gateway nodes, one limit of 100 req/s. Where does the counter live? Redis+Lua atomicity, the local-cache compromise, and failing open vs closed.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.