Infrastructure
Design Distributed Cache
01
Requirements
Requirements
- Store and fetch values by key with
get,set,delete, batched reads and an optional time to live per key. - Atomic counters and conditional writes (set only if absent, or only if the version matches).
- Simple structures beyond strings: hashes, sorted sets and lists, for sessions, leaderboards and queues.
- Capacity grows by adding nodes, with data moving to them while the cache stays online.
- When memory fills, evict the least useful keys according to a configurable policy.
- Operators can see memory, hit ratio, hot keys and replication lag per node.
- Reads and writes under 1 ms at p99 from inside the same zone.
- Survive the loss of any single node, or a whole zone, with failover in under 15 seconds.
- Linear scaling: twice the nodes give roughly twice the throughput and memory.
- Hit ratio above 95% for the main workloads, so the database is protected.
- Losing a few seconds of writes on failover is acceptable; it is a cache, not the system of record.
Ask whether it is a pure cache in front of a database, or also holds data that exists nowhere else, such as sessions, rate-limit counters or carts. A pure cache can drop everything and refill. Anything else needs persistence and stricter failover, and changes the answer to half the questions that follow.
02
Sizing the cluster
Sizing the cluster
Assume the cache sits in front of the main product database for a large consumer app. Peak load is 3M operations a second, about 85% reads, across 2.4 billion keys with values averaging around 1.2 KB.
| Quantity | Working | Result |
|---|---|---|
| Peak operations | from about 4,000 app servers | 3M / s |
| Reads and writes | 85% and 15% | 2.55M and 450K / s |
| Memory per key | 1.2 KB value + key + about 100 B of overhead | about 1.5 KB |
| Hot data set | 2.4B keys x 1.5 KB | about 3.6 TB |
| Usable RAM per node | 96 GB machine, keep a third free for fork and fragmentation | about 64 GB |
| Primary nodes | 3.6 TB / 64 GB, rounded up | 60 primaries |
| With one replica each | 60 x 2 | 120 nodes across 3 zones |
| Load per primary | 3M / 60 | about 50K ops / s, well below a node's ceiling |
| Network per primary | 50K x 1.5 KB | about 75 MB / s |
| Database load saved | 2.55M reads at a 96% hit ratio | about 100K / s reach the database |
A single in-memory node can usually handle well over 100K simple operations a second. Here, RAM sets the size of the cluster, and throughput has plenty of headroom, which is exactly what you want when one key suddenly gets hot.
A 500 GB node takes a long time to snapshot, resync or fail over, and losing it moves a large slice of traffic onto the database at once. Many medium nodes recover faster and spread the damage.
03
API design
API design
Clients speak a compact binary or text protocol over long-lived TCP connections, usually through a library that knows the cluster layout. The core operations look like this.
04
Architecture
Architecture
flowchart LR
APP["App server with cluster-aware client"] --> L1["In-process L1 cache for hot keys"]
APP --> P1["Primary A: slots 0 to 5460"]
APP --> P2["Primary B: slots 5461 to 10922"]
APP --> P3["Primary C: slots 10923 to 16383"]
P1 --> R1["Replica A, other zone"]
P2 --> R2["Replica B, other zone"]
P3 --> R3["Replica C, other zone"]
P1 <-. "gossip" .-> P2
P2 <-. "gossip" .-> P3
APP -->|miss| DB[("Primary database")]
OP["Operator and autoscaler"] --> P1The keyspace is divided into a fixed number of hash slots, 16,384 in Redis Cluster. A key belongs to slot CRC16(key) mod 16384, and each primary node owns a set of slot ranges. The slot count never changes; only which node owns each slot does. That indirection is what makes rebalancing manageable: you move slots, not individual key ranges.
Clients are cluster-aware. They download the slot map, compute the slot locally and send each command straight to the owning primary, with no proxy hop. If the map is out of date, the node replies with a redirect naming the new owner, and the client refreshes its map. Each primary streams its writes to one or more replicas in other availability zones. Nodes exchange heartbeats and state over a gossip bus, and when most primaries agree a primary is unreachable, its replica is promoted.
The database stays the system of record. Applications use the cache in a cache-aside pattern: read the cache, on a miss read the database and fill the cache with a TTL, and on writes update the database first and then delete the cached key.
A layer between keys and nodes. Adding a node means moving a few hundred slots to it, while every other key stays put.
Commands run one at a time on each node, so every command is atomic without locks. I/O threads handle network reads and writes around it.
Replicas trail their primary by milliseconds. They serve as failover targets and, for reads that tolerate slight staleness, extra read capacity.
A small in-process cache with very short TTLs in each app server absorbs the hottest few thousand keys before they reach the network.
05
Deep dive: placement, eviction, durability and failover
Deep dive: placement, eviction, durability and failover
Where does a key live, what leaves when memory is full, what survives a restart, and who takes over when a node dies? Each has a cheap default and a costly edge case.
Choosing where keys live
Plain hash(key) mod N breaks the moment N changes: adding one node to 60 remaps almost every key, and the hit ratio falls to near zero. Two schemes avoid this. Consistent hashing places nodes and keys on a ring and moves only the keys between a new node and its neighbour; with many virtual nodes per server, load evens out. Fixed slots go one step further and keep an explicit table from slot to node, which an operator or autoscaler can change slot by slot.
Moving a slot while serving traffic works in steps. The target marks the slot as importing, the source as migrating, and keys are copied in small batches. During the move, a request for a key that has already left the source gets an ASK redirect to the target for that single request; once every key is copied, ownership flips and clients receive a permanent MOVED redirect.
Multi-key commands need all their keys in one slot. Hash tags solve that: only the part of the key inside braces is hashed, so {cart:9031}:items and {cart:9031}:totals share a slot and can be used in one transaction.
Eviction and expiry
True LRU needs a linked list through every key, which costs memory and cache misses on each access. Production caches approximate it: each key keeps a small access clock, and when memory is needed the node samples a handful of keys, say ten, and evicts the oldest of them, keeping a small pool of good candidates between rounds. With sensible sample sizes, the result is very close to real LRU at a fraction of the cost.
LFU is often better for caches. A burst of one-off reads, such as a batch job scanning user ids, pushes popular keys out of an LRU but not out of an LFU, which counts how often keys are used with a counter that decays over time. Choose the policy per cluster: allkeys-lfu for a pure cache, volatile-* variants when some keys must never be evicted, and noeviction when data loss is worse than refusing writes.
Expiry runs two ways. Reading an expired key deletes it on the spot (lazy expiry). A background task also samples keys with TTLs several times a second and deletes expired ones, repeating quickly when it finds many, so memory held by keys that are never read again is reclaimed.
What survives a restart
| Option | How it works | Use it when |
|---|---|---|
| No persistence | Memory only; a restarted node comes back empty and refills from the database | Pure cache with a database that can take the refill |
| Periodic snapshots | A forked child writes the whole dataset to disk every few minutes; fork uses copy-on-write pages | Fast warm restarts; losing minutes of writes is fine |
| Append-only log | Every write appended to a file, flushed every second; rewritten in the background to stay compact | Sessions, counters or data that must lose at most about a second |
| Snapshot plus log | Log file starts with a snapshot and carries recent writes after it | The usual production choice when durability matters |
Forking a large process is the hidden cost. Copy-on-write means pages that change during the snapshot are duplicated, so a write-heavy node can briefly need far more memory. That is why the sizing above leaves a third of RAM free.
flowchart TD
W["Client sends SET to slot owner"] --> P["Primary applies in memory"]
P --> A["Reply OK to client"]
P --> S["Stream write to replica"]
S --> L{"Primary reachable?"}
L -->|yes| N["Normal operation"]
L -->|no| V["Other primaries vote on failure"]
V --> PR["Replica with most data promoted"]
PR --> M["Clients follow redirects to the new primary"]Replication and failover
Replication is asynchronous: the primary acknowledges a write before the replica has it. That keeps writes fast, and it means a failover can lose the last few milliseconds of writes. Replicas track an offset into the primary's replication stream; after a short disconnect, they resume from their offset using a backlog buffer, and only a long gap forces a full resync with a new snapshot.
Failure detection is a vote. Each node pings others, marks a silent peer as possibly failed, and spreads that opinion by gossip. When a majority of primaries agree, the failed primary's replicas start an election, and the one with the most recent data wins. A primary cut off on the minority side of a partition keeps accepting writes until it notices it has lost contact with the majority; setting a minimum number of connected replicas for writes shortens that split-brain window.
The traffic problems
Hot keys. One celebrity profile or flash-sale item can send 300K reads a second to one slot, more than any one node can serve. Spot them with per-key sampling on each node, then serve them from the client L1 cache with a TTL of a second or two, or copy the key under several suffixes (item:77#0 to item:77#7) on different nodes and pick one at random on each read.
Stampedes. When a popular key expires, thousands of requests miss at once and all go to the database. Let only one caller rebuild the value, using a short SET NX lock or in-process request coalescing, while others briefly serve the old value. Refreshing a little before expiry with a probability that grows as the TTL nears zero spreads rebuilds out further, and adding random jitter to TTLs stops many keys written together from expiring together.
Stale data. Deleting the key after a database write leaves a small race: a reader that loaded the old row just before the write can put it back after the delete. Short TTLs bound the damage. For stricter needs, feed invalidations from the database's change stream, or store a version with each value and refuse to write an older version over a newer one.
06
Anti-patterns
Anti-patterns
Asynchronous replication and eviction both drop data by design. An order or a payment that lived only in the cache is gone after a failover or a memory crunch.
Instead: keep the database as the system of record, and enable persistence plusnoeviction for the few cache-only datasets such as sessions.
A 400 GB instance takes minutes to snapshot and much longer to resync a replica. Its failure dumps a large share of traffic on the database at once.
Instead: shard across many nodes of tens of gigabytes each, so recovery is quick and the blast radius small.The command walks every key on the node while blocking all other commands, freezing a busy node for seconds.
Instead: use incrementalSCAN with a cursor, or keep an explicit index such as a set of the keys you will need to list.
Every missed invalidation leaves a stale value forever, and the cache slowly fills with keys nobody reads.
Instead: give every key a TTL with jitter, and treat explicit deletes as a way to be fresher, not the only way to be correct.07
Tradeoffs and design choices
Tradeoffs and design choices
No proxy hop, so latency stays low, and resharding moves slots without remapping everything. Clients must handle redirects and multi-key commands must share a slot.
Fast and scalableClients stay simple and any language works. Every request pays an extra hop, and the proxies are another fleet to size and keep available.
When clients cannot be upgradedThe application controls what is cached, the cache can be emptied at any time, and the database stays authoritative. Small races can leave stale values until the TTL ends.
Simple and safeThe cache is updated on every write, so reads are always warm. Write-behind can lose acknowledged writes, and both put the cache in the write path of every change.
For specific hot datasetsWrites are as fast as one node can make them. A failover can lose a few milliseconds of writes, which a cache tolerates.
Right for a cacheFewer lost writes, at the cost of latency on every write and slower progress when a replica is slow. If you need this, you may want a database.
Wrong layer for itCounters, sorted sets and hashes solve many problems in a single round trip, and commands are atomic per node.
More than key-valueMulti-threaded and very efficient for simple string caching with a slab allocator. No persistence, no replication and no structures.
Great for pure blobs08
Failure modes
Failure modes
Its slots are unavailable until a replica takes over, and writes acknowledged in the last few milliseconds are lost.
Detection and promotion complete within seconds. Clients retry with backoff and follow redirects. Applications treat the cache as lossy, so a lost write simply means an extra database read later.A primary on the minority side keeps accepting writes for a while, and the majority promotes its replica. When the partition heals, the old primary's recent writes are discarded.
Require a minimum number of reachable replicas for writes, keep node timeouts short, and place nodes so a single zone failure never takes a majority of primaries.A viral post drives a single key to hundreds of thousands of reads a second. One node saturates its CPU and every other key on it slows down.
Detect hot keys from sampling, serve them from client L1 caches with short TTLs, or replicate them under several suffixes across nodes.Thousands of concurrent misses rebuild the same expensive value, and the database slows for every user.
Single-flight rebuilds with a short lock, serve the stale value while one caller refreshes, add jitter to TTLs, and refresh early for the hottest keys.The node reports far more resident memory than data, eviction starts early and the hit ratio falls.
Enable active defragmentation, watch the fragmentation ratio, and roll restarts through replicas first when it stays high.A short network blip outlasts the replication backlog, and every replica requests a full snapshot from its primary, which forks and saturates the network.
Size the backlog for realistic outages, stagger resyncs, and alert when backlog usage approaches its limit.09
Interview tips
Interview tips
Begin with the access pattern. Read-heavy, value size, hot set and tolerance for stale data. Those four answers pick most of the design.
Explain why plain modulo hashing fails. Then present hash slots or a ring. Interviewers want to hear how little data moves when you add a node.
Talk eviction with specifics. Sampled LRU, LFU with decay, and when noeviction is correct.
Be honest about durability. Asynchronous replicas lose writes on failover. Say which data can live with that.
Solve hot keys and stampedes unprompted. L1 caches, key replication, request coalescing and TTL jitter. These are the questions that separate levels.
Know when not to use a cache. If every read must be fresh or every write durable, the cache is not the answer, and saying so shows judgement.
10
Evolution
Evolution
One memory cache next to the app. Huge win over hitting the database, until the node restarts or fills up.
Replicas in another zone and a watcher that promotes one if the primary dies. Capacity is still one node's RAM.
Data spreads over dozens of nodes with cluster-aware clients, and resharding moves slots online.
The cache tells clients when keys they hold change, so hot keys are served from app memory without going stale for long.
Invalidations come from the database change stream, hot keys are detected and spread automatically, and the cluster scales itself.
Watch and read
Try next
Free to read · better with landed
Whiteboard this with landed
landed runs it as a live system design round on the whiteboard and grades your trade-offs.

