Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free intro, Pro: 6 of 30 minutes free
Free: Requirements and estimate: throughput, not memory, sets 10 primaries. With a pass: the other 15 sections and 3 interview answers, about 24 minutes.

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:

Memory, nodes and network for the cache Python · plan/capacity_plan.py
# 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

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

  1. Question 1 of 3 A catalogue has 120 million keys averaging 1.5 KB each, with the same 25 % overhead as the lesson's plan. About how many GB of cache memory does it need, before replicas?

    Type a number.

    A hint

    Multiply keys by bytes first, then add the overhead on top.

    Show the answer: Question 1 of 3

    Answer: 225 GB (anything from 202.5 to 247.5 counts)

    120,000,000 × 1,500 bytes = 180 GB of data, and 25 % on top makes 180 × 1.25 = 225 GB.

  2. Question 2 of 3 With 64 GB nodes that may use 75 % of their memory for data, how many primaries does that 225 GB need?

    Type a number.

    A hint

    Work out what one node may hold, then round the division up.

    Show the answer: Question 2 of 3

    Answer: 5 primaries

    Each node may hold 64 × 0.75 = 48 GB, and 225 ÷ 48 = 4.7, so 5 primaries, rounded up.

  3. Question 3 of 3 The same cache serves 600,000 operations a second, and one primary handles 100,000 within the latency target. How many primaries does the cluster need in all?

    Type a number.

    A hint

    Size it twice, once for memory and once for operations, and keep the larger number.

    Show the answer: Question 3 of 3

    Answer: 6 primaries

    Throughput needs 600,000 ÷ 100,000 = 6 primaries and memory needs 5, so the larger number, 6, sets the size.

The first design: every key has one primary

Application servers send each key to its slot's primary in a cluster of 10 primaries with replicas; misses read the database.Application serverswith a cluster-aware clientslot map: 16,384 slots → primariesCache cluster: 10 primaries, each with a replicaDatabasethe source of truthPrimary 1slots 0 to 1,638Replica of 1Primary 2slots 1,639 to 3,276Replica of 2Primaries 3 to 10Their replicasasyncasyncasyncget · set with TTL · delete,sent to the key's primaryon a miss: read the database, then set the cache

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:

Interactive · message by message One read and one write of a product through the first design Step through a read that misses, the fill that follows, and a price change, from the app server to the database.
One read and one write of a product through the first design, step 8 of 8. Messages so far: App server to Primary 3: get product:4; Primary 3 to App server: miss; App server to Database: read product 4; App server to Primary 3: set product:4, TTL; App server to Primary 3: get product:4; App server to Database: update the price; App server to Primary 3: delete product:4.App serverslot 4,261: primary 3Primary 3product:4 deletedDatabase40,000 reads a secondget product:4missread product 4set product:4, TTLget product:4update the pricedelete product:4

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. 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.
  2. Ask the owner. The get goes straight to primary 3, the only node that serves slot 4,261. No other node is asked.
  3. A miss. The entry has expired, so primary 3 answers with a miss, in well under a millisecond.
  4. 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.
  5. 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.
  6. 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.
  7. A price change. When the price changes, the app writes the database first, because the database is the source of truth.
  8. 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:

  1. How does a client find the node for a key, and what happens while slots move to a new node?
  2. What can a failover lose, and how do you stop a deleted value from coming back?
  3. How much memory should each node use, which eviction policy fits, and what about one very hot key?
  4. How does the cache stay consistent with the database when writes and reads race?
  5. What happens to the database when a node dies, or when the whole cluster starts empty?
  6. 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

  1. Question 1 of 2 Why does the failure of one primary out of 10 matter so much, when 90 % of the cache is still healthy?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A tenth of all reads lose their cache at once and must be served by something else, usually the database

    Each primary owns a range of the slots. Until something takes over the failed primary's range, every read of its keys misses, and a tenth of a million operations a second is far more than a database built for a warm cache can take.

  2. Question 2 of 2 One team's clients are short-lived functions that cannot keep a map of where keys live. Which option of the first design suits them best?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A proxy tier, which knows where every key lives

    Client-side hashing puts the map of keys to nodes in every client. A proxy tier keeps that map in the proxies, so a client can send any key to any proxy and keep nothing itself.

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.

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).

Read a complete sample first: Design a unique ID generator

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.