System Design (High-Level Design) Module 19 – Building blocks: traffic, storage and messaging
Design a distributed cache
Design a distributed cache from requirements and a memory estimate to sharding, routing, replicas, eviction, hot keys, consistency and failures.
What you will learn
- Estimate the memory, nodes and network a cache cluster needs from keys, item sizes and operations
- Partition a cache across nodes with client-side hashing, a proxy tier or hash slots with redirects
- Add replicas and failover without serving stale data
- Plan memory limits, eviction policies and hot-key handling
- Protect the database when a cache node dies or the whole cluster starts cold
Before you start
What you’ll learn · Before you start
What you will learn
- Estimate the memory, nodes and network a cache cluster needs from keys, item sizes and operations
- Partition a cache across nodes with client-side hashing, a proxy tier or hash slots with redirects
- Add replicas and failover without serving stale data
- Plan memory limits, eviction policies and hot-key handling
- Protect the database when a cache node dies or the whole cluster starts cold
Before you start
On this page
A distributed cache: A cache run as a separate service, such as Redis, Valkey or Memcached, that every application instance reads over the network: one copy for the whole fleet, kept through application restarts and deploys. More in “Where caches live, from browser to database” keeps copies of frequently read data in the memory of many machines, so that most reads are answered in well under a millisecond and never reach the database. In the design below, 10 primaries and their 10 replicas answer a million operations a second in front of a database that can serve 40,000 reads a second.
The design spreads keys over nodes so that capacity grows with the cluster, keeps a replica of each node so that one failure does not empty part of the cache, evicts entries when memory runs out, and stays close enough to the database that readers do not see stale data for long. Memcached, Redis Cluster and Valkey, a BSD-licensed open-source key-value store (Valkey), are the usual building blocks.
Requirements and estimate: throughput, not memory, sets 10 primaries
A shopping app serves product pages, prices and sessions from a database that can answer about 40,000 reads a second. At peak the app makes a million cache operations a second. Before drawing anything, ask:
- What is cached, and what is the source of truth for it?
- How large are the items, and how many keys are there?
- What share of operations are reads, and how stale may a read be?
- May a cache node lose its contents, or must the cache survive restarts?
- One region or several, and what happens to the database if the cache is empty?
The answers here: 200 million keys of about 1 KB each, 90 % reads, a price may be stale for a few seconds at most, the cache can be rebuilt from the database, and there is one region for now.
Functional requirements: get, set with a TTL (cache entry): Time to live: how long a cache may serve an entry after storing it. When it runs out the next read is a miss, so even a copy that a missed delete left behind disappears. More in “Cache-aside, read-through, write-through, write-back”, delete, and multi-get; eviction when memory is full. Non-functional requirements: p99 latency under 1 ms inside a zone, a million operations a second, no outage when one node fails, room to grow, and a database that is never sent more reads than it can serve.
The first estimate, with every assumption printed:
# A first capacity plan for the distributed cache of this lesson. Every input is an assumption to replace with
# measurements: item sizes from a sample of real keys, overhead from the cache's own memory statistics, and the
# operations a node serves within the latency target from a load test on the hardware you will use.
KEYS = 200_000_000
BYTES_PER_ITEM = 1_000 # key and value together, on average
OVERHEAD = 0.25 # per-key structures and allocator fragmentation, on top of the data
NODE_RAM_GB = 64
MEMORY_LIMIT = 0.75 # share of RAM given to data; the rest is for buffers, growth and snapshots
OPS_PER_SECOND = 1_000_000 # 90 % reads
OPS_PER_PRIMARY = 100_000 # within a p99 of 1 ms
GB = 10**9
data = KEYS * BYTES_PER_ITEM / GB
total = data * (1 + OVERHEAD)
usable = NODE_RAM_GB * MEMORY_LIMIT
for_memory = -(-total // usable) # ceiling division
for_throughput = -(-OPS_PER_SECOND // OPS_PER_PRIMARY)
primaries = int(max(for_memory, for_throughput))
print(f"data: {KEYS:,} keys × {BYTES_PER_ITEM:,} bytes = {data:,.0f} GB")
print(f"with {OVERHEAD:.0%} overhead: {total:,.0f} GB")
print(f"usable per {NODE_RAM_GB} GB node at a {MEMORY_LIMIT:.0%} limit: {usable:.0f} GB -> {for_memory:.0f} primaries for memory")
print(f"{OPS_PER_SECOND:,} operations a second at {OPS_PER_PRIMARY:,} per primary -> {for_throughput} primaries for throughput")
print(f"plan: {primaries} primaries holding {total / primaries:.0f} GB each, plus one replica each = {2 * primaries} nodes")
print(f"network: {OPS_PER_SECOND * BYTES_PER_ITEM / GB:.1f} GB/s in total, "
f"{OPS_PER_SECOND * BYTES_PER_ITEM * 8 / GB / primaries:.1f} Gbit/s per primary")
print(f"one primary lost: {1 / primaries:.0%} of the keys miss until its replica takes over") Output
data: 200,000,000 keys × 1,000 bytes = 200 GB with 25% overhead: 250 GB usable per 64 GB node at a 75% limit: 48 GB -> 6 primaries for memory 1,000,000 operations a second at 100,000 per primary -> 10 primaries for throughput plan: 10 primaries holding 25 GB each, plus one replica each = 20 nodes network: 1.0 GB/s in total, 0.8 Gbit/s per primary one primary lost: 10% of the keys miss until its replica takes over
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 capacity_plan.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Memory alone asks for 6 primaries, but throughput asks for 10, so throughput decides the size. Losing one primary sends a tenth of all reads somewhere else, and that number drives the failure design. Try the same arithmetic on another catalogue:
Checkpoint
Check the estimate
The first design: every key has one primary
The first design of the distributed cache
Text description of the diagram
The diagram shows the first design from top to bottom.
- At the top are the application servers. Each uses a cluster-aware client that keeps a slot map: which primary owns each of the 16,384 hash slots.
- In the middle is the cache cluster: 10 primaries, each owning a range of slots (primary 1 owns slots 0 to 1,638, primary 2 slots 1,639 to 3,276, and so on), and each sending its writes to its own replica by asynchronous replication.
- The application servers send get, set with a TTL, and delete to the primary that owns the key's slot.
- At the bottom is the database, the source of truth. A dashed arrow shows that on a cache miss the application reads the database, then sets the value in the cache.
Every key belongs to one of 16,384 One of the 16,384 numbered buckets that a Redis-style cluster maps every key into. Each slot is served by one primary, and a node that does not serve a key's slot redirects the client to the node that does., and every slot to one primary. Follow one read and one write of a product through the design, from the app server to the database:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- Which primary?. A product page needs product:4. The client library maps the key to hash slot 4,261, and its slot map says primary 3 owns that slot.
- Ask the owner. The get goes straight to primary 3, the only node that serves slot 4,261. No other node is asked.
- A miss. The entry has expired, so primary 3 answers with a miss, in well under a millisecond.
- Read the source of truth. The app reads the product from the database, which can serve 40,000 reads a second for the whole shop.
- Fill with a TTL. The app stores the value on primary 3 with a time to live, so even a copy that nobody deletes cannot live for ever.
- Hits until it changes. Every later read of product:4 is answered by primary 3 until the entry expires or is deleted, and the database does no work for it.
- A price change. When the price changes, the app writes the database first, because the database is the source of truth.
- Then delete the copy. Then it deletes the cached copy, so the next read misses and loads the new price. Whether that delete can be lost is one of the deep dives below.
The design leaves several choices open, each with one main option:
- Client-side hashing: the client library maps a key to a node itself.
- A proxy tier: clients send every request to proxies that know where keys live.
- Hash slots with redirects: keys map to 16,384 slots; a node that does not own a slot redirects the client.
- Asynchronous replicas: each primary copies its writes to a replica, which takes over when the primary fails.
- Eviction policies: rules that choose which entries to drop when memory is full.
- Leases: tokens that stop a slow reader from writing an outdated value back into the cache.
- A standby pool: a few spare nodes that take over the keys of a failed node.
The deep dives that decide whether this design works:
- How does a client find the node for a key, and what happens while slots move to a new node?
- What can a failover lose, and how do you stop a deleted value from coming back?
- How much memory should each node use, which eviction policy fits, and what about one very hot key?
- How does the cache stay consistent with the database when writes and reads race?
- What happens to the database when a node dies, or when the whole cluster starts empty?
- How does a second region get a cache without slow reads across regions?
An answer is judged on requirements and estimates, partitioning and routing, replication and failover, memory and eviction, consistency with the database, and failure handling.
Checkpoint
Check the first design
The API: five calls, and a TTL on every key Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Finding a key’s node: three ways, and why hash slots move few keys
- A slot moves key by key: MOVED updates the map, ASK redirects one read
- An asynchronous failover can lose a delete and bring an old price back
- Leave a quarter of memory free, and choose a policy that evicts
- A hot key needs copies, because no new node splits it
- Leases stop a slow reader from writing an old value back
- A dead primary is a database problem
- One primary of 10 dies at peak
- The whole cluster starts empty
- A move that stops halfway, and a partition
- Growing past one cluster: 10×, 100× and a second region
- Four alternatives, and why each loses
- What each level is expected to cover
- A sample answer and a 45-minute self-mock
- Practise: route by slot and follow redirects
- Route reads by hash slot and follow MOVED and ASK
- Interview questions
- A cache of 10 primaries serves a million operations a second in front of a database that can answer 40,000 reads a second. One primary dies at peak, and the database falls over although 90 % of the cache is healthy. Explain why, and redesign the failover so that the database survives.
- Every price change in your shop updates the database and then deletes the cached price. After a cache primary failed over, customers saw old prices on a few products for several minutes. Nothing else changed. What happened, and how do you prevent it?
- You grow a hash-slot cache cluster from 6 to 8 primaries during business hours. Midway, one service logs errors on multi-key reads, and another sees its p99 latency double while its hit ratio stays the same. Explain what each team sees, and how you run the migration safely.
- Key takeaways
3 diagrams · 4 runnable examples (Python) · 1 exercise with sample tests · 3 interview answers · about 57 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- Redis cluster specification (Redis)
- Key eviction (Redis)
- WAIT (command reference) (Redis)
- Scaling Memcache at Facebook (USENIX NSDI (Nishtala and others))
- Optimal Probabilistic Cache Stampede Prevention (Proceedings of the VLDB Endowment (Vattani, Chierichetti and Lowenstein; authors' copy))
- Valkey (Valkey project (Linux Foundation))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress