0% completed
Design a Distributed Cache (like Redis)
On This Page
1. Problem Definition and Scope
2. Clarify functional requirements
3. Clarify non-functional requirements
4. Back of the envelope estimates
5. API design
6. High level architecture
7. Data model
8. Core flows end to end
Flow 1: Saving Data (The "SET" Request)
Flow 2: Retrieving Data (The "GET" Request)
Flow 3: The Cache Miss (Reading from the Database)
9. Caching and read performance
10. Storage, indexing and media
11. Scaling strategies
12. Reliability, failure handling and backpressure
13. Security, privacy and abuse
14. Bottlenecks and next steps
The complete picture
Summary
The decisions that mattered
The numbers that forced them
What breaks first
If the interviewer pushes
This lesson designs a distributed cache, a store that keeps data in memory on many servers so that applications can read it much faster than from a database. Redis and Memcached are the best known examples. The whole design follows from one target: a read or a write must finish in well under a millisecond, so every extra step on the request path has to justify itself.
1. Problem Definition and Scope
We are designing a distributed, in-memory key-value store. A key-value store saves each value under a unique name, the key, and finds it again by that name. Our store sits between application services and the main database. It answers repeated reads quickly, which lowers response time and reduces the load on the database.
- User Groups: Backend services and application servers. The users of this system are programs, not people.
- Main Actions: Storing a value (Put), reading a value (Get), and removing a value (Delete).
- Scope:
- In Scope: The distributed part of the problem, which means splitting the data across many servers (sharding) and keeping copies of it (replication).
- Trade-off: We favor availability and low latency over strict consistency. When a server fails, the cache keeps serving and may lose a few recent writes, instead of stopping until every copy agrees. In CAP terms (Consistency, Availability, Partition tolerance), this is closer to an AP system than a CP one.
- Out of Scope: Complex data structures such as sorted sets and geo indexes, and advanced features such as Pub/Sub messaging and Lua scripting. We focus only on storing and reading values by key.
The scope matters because a cache is never the only copy of the data. The database remains the source of truth, which means that if the two disagree, the database is correct. That single fact is what allows the cache to trade safety for speed in the rest of the design.
2. Clarify functional requirements
Functional requirements list what the system must do for its users.
Must Have:
- Put (Key, Value, TTL): A client stores a value under a text key. The value is treated as raw bytes. The client can add a time to live (TTL), which is the number of seconds after which the key expires.
- Get (Key): A client reads the value, if the key exists and has not expired.
- Delete (Key): A client removes a key on purpose, for example after the database row behind it changes.
- Eviction: When memory is full, the system removes the least recently used (LRU) keys, meaning the keys that nobody has read or written for the longest time.
- High Availability: If a server fails, its keys stay reachable through a copy (a replica) on another server.
Nice to Have:
- Persistence: The cache saves a copy of its memory to disk from time to time, so that after a restart it starts with data already loaded (a "warm" restart) instead of empty.
- Cluster Awareness: The client library knows which server holds which key, so it can send each request straight to the right server. Section 6 shows that our latency target makes this a necessity, so we build it into the core design.
Notice that these requirements say nothing about keeping data forever. A cache may lose a value at any time, through expiry, eviction or a crash, and the application must always be able to read it again from the database.
3. Clarify non-functional requirements
Non-functional requirements describe how well the system must work: how big, how fast, and how reliable.
- Latency: Latency is the time a request takes. The work on a cache server must take under 1 ms for 99 percent of reads and writes (the 99th percentile, or p99), not counting the time on the network.
- Throughput: The cluster must handle 10 million queries per second (QPS) in total. The cluster runs inside one region, because a request to another region takes tens of milliseconds and would miss the latency target by far.
- Availability: 99.9%, often called "three nines". This allows about 8.8 hours of downtime a year. Short failures are acceptable because the database still holds every value, but each one sends extra load to the database.
- Consistency:
- On the primary: Every key has one primary server, and all reads and writes for that key go to it. In normal operation, a client therefore reads its own write at once. This is called read-your-own-writes consistency. It is not strong consistency, because a failover can still lose writes that had not reached a replica yet.
- On replicas: A replica may be a few milliseconds behind its primary. This is eventual consistency, meaning the copies agree after a short delay. Clients do not read from replicas, so this delay matters only after a failover.
- Data Size: The cluster must hold about 10 TB of data in RAM.
The latency target is the most important number on this list. Under 1 ms leaves no room for an extra network hop, for waiting on a replica, or for threads waiting on each other.
4. Back of the envelope estimates
These estimates are rough on purpose. Their job is to find which resource limits the size of the cluster.
- Storage Estimates:
- Target data: 10 TB.
- Replication factor: 3, meaning each key is stored three times: on one primary and on two replicas.
- Total RAM needed: 10 TB × 3 = 30 TB.
- RAM per node: we assume servers with 64 GB of RAM, of which about 50 GB holds cache data. The other 14 GB is left for the operating system, for connection buffers, and for the extra memory a disk snapshot can need (section 10).
- Total nodes: 30,000 GB ÷ 50 GB = 600 nodes.
- The 10 TB must include the bookkeeping for each key, such as pointers and the expiry time. At 1 KB per value this is roughly 5 to 10 percent extra, so a real plan would add about 10 percent more nodes.
- Throughput Estimates:
- Total QPS: 10 million.
- Each node is the primary for about one third of the data it holds, and a replica for the rest (section 11 explains how). Client traffic therefore spreads over all 600 nodes.
- QPS per node: 10,000,000 ÷ 600 ≈ 16,700 QPS.
- Feasibility: One cache process such as Redis can serve roughly 100,000 simple operations per second on one CPU core, and more with pipelining, which means sending many commands without waiting for each reply. Our load per node is a small part of that. The cluster size is set by memory, not by processing power: we are memory bound, not CPU bound.
- Bandwidth Estimates:
- Average value size: 1 KB.
- Total bandwidth: 10 million QPS × 1 KB ≈ 10 GB per second.
- Bandwidth per node: 10 GB per second ÷ 600 ≈ 17 MB per second. A standard 10 Gbps network card moves about 1,250 MB per second, so each node uses less than 2 percent of it.
The conclusion is that we buy servers for their memory. Adding CPU cores or faster network cards would not reduce the number of nodes.
5. API design
The API is the set of requests that applications send to the cache. Real caches use their own compact protocols over TCP connections that stay open. Redis, for example, uses RESP (the Redis serialization protocol), a simple text format that can still carry any bytes. For clarity, we describe the same operations here as REST requests, which are plain HTTP requests on named URLs.
1. Set Value
- Method:
PUT /v1/keys/{key} - Body:
{"value": "base64_data", "ttl_seconds": 3600}. The value is encoded in base64, a way of writing raw bytes as plain text. - Response:
200 OKwhen the value is stored, or413 Payload Too Largewhen the value is over the size limit, for example 1 MB.
2. Get Value
- Method:
GET /v1/keys/{key} - Response:
200 OK:{"value": "base64_data", "ttl": 3590}, wherettlis the number of seconds the key has left.404 Not Found: the key does not exist or has expired. For the application, both mean the same thing: read the database.
3. Delete Value
- Method:
DELETE /v1/keys/{key} - Response:
204 No Content, whether or not the key existed. Deleting a key twice therefore has the same effect as deleting it once, so a client can safely retry.
6. High level architecture
The usual way to spread requests across many servers is a load balancer, a server that receives every request and forwards it to one of the servers behind it. We do not use one. The work on a cache server takes microseconds, while each extra network hop inside a data center adds about 0.1 to 0.5 ms. A load balancer would therefore cost more time than the cache operation itself. Instead, we use a smart client: the application finds the right cache server by itself and sends the request straight to it.
Components:
- Smart Client (SDK): A library inside each application server, for example in Service A and Service B. It holds a cluster map, a table that says which cache node owns which range of keys. It uses consistent hashing (section 11) to find the owner of each key. Memcached clients have worked this way for a long time.
- Cache Nodes: The 600 servers that hold the data in RAM. They share no memory or disk, and they do not coordinate with each other when they answer a request. The only traffic between them is replication: each node copies its writes to the replicas of its keys.
- Cluster Manager: A small, highly available coordination service, such as ZooKeeper or etcd. It checks that every cache node is alive, decides what happens when one fails, and sends updates of the cluster map to every smart client.
The cluster manager is never on the path of a read or a write. Clients ask it only for the cluster map, so a slow manager delays map updates but does not slow down any cache request.
7. Data model
Because all data lives in memory, the "data model" here means the structures inside each node's RAM. The goal is that every operation takes constant time, written O(1), which means the work does not grow as the number of keys grows. Each node uses two structures over the same entries.
1. Hash Map (The Index)
- Purpose: Finds any key in one step.
- Key: The text key from the client.
- Value: A pointer to the entry in memory. The entry holds the value, the expiry time, and the two links described next.
2. Doubly Linked List (The Eviction Queue)
- Purpose: Keeps the entries in the order they were last used, so the node always knows which one to evict. A doubly linked list is a chain in which each entry points to the entry before it and the entry after it, so any entry can be removed in one step.
- Head: The most recently used (MRU) entry.
- Tail: The least recently used (LRU) entry.
- Mechanism: Every time a key is read or written, its entry moves to the head. When memory is full, the node removes the entry at the tail.
The hash map alone has no order, so it cannot say which key is the oldest. The list alone cannot find a key without walking through it. Together, they give constant time lookup and constant time eviction.
Redis itself does not keep this list, because two extra pointers per key cost a lot of memory across billions of keys. Instead, it samples a few keys (5 by default) and evicts the one used least recently, which comes close to true LRU. Redis also does not evict at all by default: its default policy, noeviction, rejects new writes when memory is full. A cache must set a policy such as allkeys-lru or allkeys-lfu (least frequently used).
8. Core flows end to end
This section follows the three most important journeys through the system: saving a value, reading a value, and handling a missing value. For each step, we describe what happens, why the step is needed, and what it produces.
Every flow begins in the smart client. The client already holds the cluster map, so it knows which server owns a key before it sends anything. No other server takes part in choosing the destination.
Flow 1: Saving Data (The "SET" Request)
Scenario: A user logs in to a website. The backend, Service A, saves the user's session under the key user:123, with the value {"status": "active"} and a TTL of 1 hour.
Step 1: Find the node that owns the key (hashing)
What happens: The smart client runs the key user:123 through a hash function, a formula that turns any text into a number. It then looks up that number in its cluster map. The answer is that Server #42 is the primary for this key.
Why: There are 600 servers. If the client picked one at random, the next read would not know where to look. A hash always gives the same number for the same key, so every client finds the same server.
Outcome: The client knows the owner of the key without asking any other server.
Step 2: Send the request straight to the owner (routing)
What happens: The client sends the write to Server #42 over a TCP connection that it keeps open for many requests (a connection pool). The request goes over the private network inside the data center. It passes through no load balancer or gateway.
Why: Opening a new connection for every request would add a full extra round trip, and a middle server would add a network hop. Either one would cost more time than the cache operation itself.
Outcome: The write arrives at the correct server after a single network hop.
Step 3: Make room if memory is full (eviction)
What happens: Before storing the value, Server #42 checks whether it has reached its memory limit. If it has, it removes the entry at the tail of its linked list, the key that has gone unused the longest. It repeats this until there is enough room. Removing keys to make room is called eviction.
Why: A cache that refuses writes when it is full stops being useful. Removing the least recently used key keeps the keys that applications are actually reading.
Outcome: The node has room for the new value, and the key it evicted can still be read from the database.
Step 4: Store the value in memory
What happens: Server #42 updates its two structures:
- Hash map: It stores the entry for
user:123, with the value and an expiry time of now plus 1 hour. - Linked list: It places the entry at the head of the list, which marks it as the most recently used key.
Why: The hash map lets the next read find the key in one step. The position at the head of the list protects the key from eviction while it is still in use.
Outcome: The value is stored and can be read at once.
Step 5: Reply first, then copy to the replicas (asynchronous replication)
What happens: Server #42 replies "success" to Service A as soon as the value is in its own memory. Right after replying, it sends the write to the key's two replicas, for example Server #43 and Server #87. On a fast local network, this copy usually arrives within about a millisecond. This is called asynchronous replication, because the reply does not wait for the copy.
Why: Waiting for a replica to confirm would add a second network round trip to every write, which roughly doubles the write latency. We give up some durability, meaning the guarantee that an acknowledged write survives a crash, in return for speed.
Outcome: The client has its answer in well under a millisecond. If Server #42 crashes in the short time before the copy arrives, that write is lost. A cache can accept this, because the database still holds the data.
Flow 2: Retrieving Data (The "GET" Request)
Scenario: The user refreshes the page. Service A needs to read user:123.
Step 1: Find the owner again
What happens: The smart client runs user:123 through the same hash function and the same cluster map. It gets the same answer, Server #42, and sends the read there.
Why: Reads must go to the same server that took the write. All reads for a key go to its primary, never to a replica, so the client always sees the newest value the cache holds.
Outcome: The read arrives at the server that holds the key.
Step 2: Look up the key
What happens: Server #42 looks up user:123 in its hash map. If the key is not there, it replies 404 Not Found. If the key is there, the server takes the entry.
Why: The hash map finds the key in one step, however many keys the node holds.
Outcome: The server either has the entry or has already answered that the key is missing.
Step 3: Check the expiry time (lazy deletion)
What happens: The server compares the entry's expiry time with the current time. If the expiry time has passed, the server deletes the entry and replies 404 Not Found. Deleting an expired key only at the moment someone reads it is called lazy deletion. If the key has not expired, the server continues.
Why: Checking expiry during the read costs almost nothing, and it guarantees that a client never receives an expired value.
Outcome: The server holds a value that is still valid, or it has removed an expired one.
Step 4: Mark the key as recently used
What happens: Even though this is a read, the server changes its memory. It moves the entry for user:123 from its current place in the linked list back to the head.
Why: The eviction order must reflect reads as well as writes. Otherwise, a key that is read constantly but rarely written would move to the tail and be evicted. This step also means that every read changes shared data, which matters for the threading choice in section 9.
Outcome: The key is protected from eviction for as long as applications keep reading it.
Step 5: Return the value
What happens: Server #42 sends the value back to Service A.
Why: This completes the request.
Outcome: The work on the server takes a few microseconds. The whole round trip, including the network inside the data center, usually stays under 1 millisecond.
Flow 3: The Cache Miss (Reading from the Database)
Scenario: The key is not in the cache, because it expired, was evicted, or was never stored. This flow follows the cache-aside pattern (also called look-aside), in which the application, not the cache, reads the database and fills the cache.
Step 1: The cache reports a miss
What happens: Service A asks Server #42 for user:123, and Server #42 replies 404 Not Found. This is a cache miss.
Why: The cache never reads the database by itself. It only knows what it holds.
Outcome: Service A knows that it must find the data elsewhere.
Step 2: Read the database
What happens: Service A reads the user from the main database, for example PostgreSQL. This read is slower, often several milliseconds, but the database always has the data.
Why: The database is the source of truth. Any value missing from the cache can always be found there.
Outcome: Service A has the correct, current value.
Step 3: Fill the cache
What happens: Service A sends a SET request (Flow 1) to Server #42 with the value it just read and a TTL.
Why: Without this step, every future read of the key would also go to the database. The TTL limits how long the cached copy can stay out of date if the database changes later.
Outcome: The cache holds the value again.
Step 4: The next read is a hit
What happens: The next time any service reads user:123, Server #42 returns it from memory, as in Flow 2.
Why: This is the reason the cache exists: one slow database read now serves many fast reads.
Outcome: The database is read once per miss, not once per request.
9. Caching and read performance
This section describes how each node keeps every operation fast.
- Single-Threaded Event Loop:
- Each node runs all commands on one thread, as Redis does. An event loop is a loop that waits for any open connection to have data, then handles the requests that are ready, one at a time. The operating system tells the loop which connections are ready through I/O multiplexing (
epollon Linux,kqueueon BSD and macOS). - Why? As Flow 2 showed, even a read changes the linked list. With many threads, every operation would need a lock, a guard that lets only one thread change the data at a time. Each operation takes less than a microsecond of memory work, so the locks and the waiting would cost more than the work they protect. One thread needs no locks at all.
- To use the other cores of a machine, we run several cache processes on it, not more threads in one process. Redis does add optional I/O threads (since version 6) that read requests from the network and write replies. The commands themselves still run one at a time on the main thread.
- Each node runs all commands on one thread, as Redis does. An event loop is a loop that waits for any open connection to have data, then handles the requests that are ready, one at a time. The operating system tells the loop which connections are ready through I/O multiplexing (
- Memory Fragmentation:
- When values of very different sizes are stored and removed, for example 10 bytes and then 5 KB, the free memory breaks into many small gaps that are too small to reuse. This is called fragmentation, and it wastes RAM.
- Solution: Use a memory allocator such as jemalloc, which Redis uses by default on Linux. It groups memory into many fixed size classes, so a freed block is reused by a later value of a similar size.
- Expiration Strategy:
- Lazy deletion: A key is checked when someone reads it, as in Flow 2.
- Active sampling: Lazy deletion cannot remove a key that nobody reads again. So, 10 times a second, a background task picks 20 random keys from the keys that have a TTL and deletes the expired ones. If more than a quarter of the sample had expired, it repeats at once. This way, expired keys that nobody reads do not fill memory forever.
10. Storage, indexing and media
- Primary storage: RAM. Its contents are lost when the process stops.
- Persistence (optional):
- Snapshots (RDB files): Every 15 minutes, the process forks, meaning it creates a copy of itself, and the copy writes the whole memory to a file on disk. The original process keeps serving requests. The operating system shares memory pages between the two processes and copies a page only when the original changes it. Under heavy writes, this copying can use a large amount of extra memory, which is one reason we keep 14 GB free on each node.
- Append-only file (AOF): Redis can also log every write to a file. With one disk flush per second, a crash loses at most about one second of writes. A cache rarely needs this, because the database already holds every value.
- On startup, a node can load its last snapshot to start "warm". The snapshot can be up to 15 minutes old, so it may contain values that changed in the database since then. The TTLs limit how long such stale values survive.
- Media handling:
- We do not store large images or videos in the cache. They fill RAM quickly, and sending one large value keeps the single thread busy, so every other request on that node waits.
- Strategy: Store media in object storage such as Amazon S3, and cache only its URL, about 100 bytes.
11. Scaling strategies
We use consistent hashing to spread data across the 600 nodes. Consistent hashing is a way of assigning keys to servers so that adding or removing a server moves only a small share of the keys.
- The Ring:
- Imagine a circle of numbers from 0 to 4,294,967,295, which is 2 to the power 32, minus 1. After the largest number, the circle starts again at 0.
- Each server is placed on the ring at the position
hash(server_id). - A key is placed at
hash(key). Its primary is the first server found by moving clockwise from that position. Its two replicas are the next two different physical servers clockwise. - Because of this rule, every node is the primary for some ranges of the ring and a replica for the ranges of the servers just before it. This is why client traffic spreads over all 600 nodes, as section 4 assumed.
- Virtual Nodes:
- Problem: With only one position per server, the gaps between servers on the ring are very uneven. One server might own 40 percent of the keys while another owns 10 percent.
- Solution: Each physical server is placed on the ring 100 times, at 100 different positions called virtual nodes. Many small ranges per server average out, so every server gets a nearly equal share of keys and load.
- The ring then has 600 × 100 = 60,000 positions. The client keeps them in a sorted list and finds the owner of a key with a binary search, about 16 comparisons.
- Adding/Removing Nodes:
- When a 601st node joins, its 100 virtual nodes each take a small range from whichever server was next on the ring. The new node ends up with about 1/601 of all keys, collected in small pieces from many different servers. Every other key stays where it is.
- With a simple rule such as
hash(key) % number_of_servers, changing 600 servers to 601 would move almost every key, and nearly every read would become a miss at the same moment.
- How Redis Cluster does it: Redis Cluster uses a related idea with fixed hash slots instead of a ring. Every key maps to one of 16,384 slots, using
CRC16(key) mod 16384, and each primary owns a set of slots. Adding a node means moving some whole slots to it, so again only a small share of the keys moves.
12. Reliability, failure handling and backpressure
Backpressure means limiting new work when the parts that process it are already full. For a cache, the main risk is passing too much load to the database, so this section is mostly about protecting the database.
- Failure Detection:
- The cluster manager sends a heartbeat, a small "are you alive?" message, to every cache node, for example once a second.
- If a node misses 3 heartbeats in a row, the manager marks it as dead.
- Failover:
- Failover is the act of moving a failed server's work to another server. For each range the dead node owned, the manager promotes one of its replicas to be the new primary. It chooses the replica that has received the most recent writes.
- The manager then sends the new cluster map to all smart clients. A client that still holds an old map may send a request to the wrong node. That node replies that it does not own the key and names the new owner, as Redis does with its
MOVEDreply, and the client updates its map. - Until the map is updated, requests for the failed node's keys fail. The client treats such a failure as a cache miss and reads the database, so users see slower pages, not errors.
- Writes that the old primary acknowledged but had not yet copied are lost. This is the cost of asynchronous replication from Flow 1.
- Avoiding two primaries:
- A node can be cut off from the manager while still reaching some clients. If it kept accepting writes, two nodes would act as primary for the same keys. To prevent this, a node that loses contact with the manager stops accepting writes after a short timeout.
- The manager itself must never make two conflicting decisions. ZooKeeper and etcd run as 3 or 5 servers and make every decision by majority vote, called a quorum. A group of 5 keeps working with 2 servers down.
- Redis Cluster has no separate manager. Its nodes exchange health information with each other, and a failover needs the votes of a majority of the primaries.
- Thundering Herd (Cache Stampede):
- Problem: A thundering herd, or cache stampede, is many requests for the same missing key at the same moment. When a popular key expires, thousands of application threads may miss together and all query the database for the same row.
- Solution: Request coalescing in the client library, also called singleflight. If 100 threads in one application server want the same missing key, the library sends one database query and gives its result to all 100 threads.
- Coalescing works inside one application server. With 1,000 application servers, the database can still receive up to 1,000 queries for the key. If that is too many, the first client to miss can set a short lock key in the cache, and the others wait briefly and read the cache again.
13. Security, privacy and abuse
- Network Isolation: The cache cluster runs in a private network (a VPC, or virtual private cloud). Its ports are not reachable from the public internet.
- Authentication: Each client must prove who it is when it opens a connection, for example with a password or a username and password (the
AUTHcommand in Redis). - Encryption: Data travels over TLS, the standard encryption for network connections. Because connections stay open, the setup cost is paid once per connection, and the extra time per request is small. Many compliance rules require it.
- Namespace Isolation: If several teams share the cluster, each team puts a prefix on its keys, for example
billing:user:1andshipping:user:1. Two teams then never overwrite each other's keys by accident.
14. Bottlenecks and next steps
- Bottleneck: The "Celebrity" Key:
- A hot key is a single key that receives far more traffic than the others. If a famous singer's profile is read 1 million times a second, all of those reads go to the one node that is its primary. One node serves about 100,000 operations a second, so it cannot serve them all.
- Adding nodes does not help, because consistent hashing spreads different keys, and this is one key. Adding replicas does not help either, because in this design all reads for a key go to its primary.
- Next Step: Add a local cache (near cache). The smart client detects hot keys and keeps a copy of them in the application server's own memory for a few seconds. Most reads then never leave the application server. The cost is that the copy can be a few seconds out of date. Redis supports this pattern with client-side caching, where the server tells the client when a cached key changes.
- Bottleneck: RAM Cost:
- RAM costs far more per gigabyte than disk.
- Next Step: Use tiered storage. Keep the most used data in RAM and less used data on flash (NVMe SSD) drives, using a storage engine such as RocksDB or a product such as Redis on Flash. Reads from flash take longer, often around 0.1 ms, but the cost per gigabyte is much lower.
The complete picture
Every diagram above shows one part of the system. This one shows all of them together: the smart client and its single hop to the right node, the two structures inside each node, the replication and failover path, and the near cache for hot keys. Notice that the database appears only on the miss path, and that the cluster manager never sits on the path of a read or a write.
Summary
Say this first. A distributed cache is one of the few systems where the client is part of the architecture. There is no load balancer in front. The smart client hashes the key, knows which of the 600 nodes is its primary, and sends the request straight there. Every other design choice exists to protect that single hop and the sub-millisecond target behind it.
The decisions that mattered
| Decision | Why | What it costs |
|---|---|---|
| Smart client with consistent hashing | A load balancer would add a network hop that costs more than the cache operation | Every application language needs the library, and every client needs cluster map updates |
| Single threaded event loop per node | Locks would cost more than the memory work they protect | One core per process, so we run several processes per machine and add machines to grow |
| A hash map plus a doubly linked list | Constant time lookup and constant time eviction over the same entries | Two extra pointers per key, and both structures change on every access |
| Asynchronous replication | Waiting for a replica would add a second round trip to every write | A crash in that short window loses the write, which a cache can accept |
| Virtual nodes on the ring | Without them, a few nodes get far more keys than others | A larger ring of 60,000 positions for every client to hold and search |
The numbers that forced them
| Number | What it decided |
|---|---|
| 10 TB of data, stored three times | 600 nodes at about 50 GB of usable memory each |
| About 16,700 QPS per node | We are memory bound, not CPU bound, so we buy servers for their RAM |
| Under 1 millisecond target | No extra hop, no locks, and no waiting for replicas |
| 17 MB per second per node | Bandwidth is not a constraint, so it needs no design time |
What breaks first
- The celebrity key. One key read a million times a second goes to one node, and sharding cannot spread a single key. A near cache inside the application for a few seconds is the answer.
- A cache stampede. A popular key expires and every client goes to the database at once. Request coalescing in the client sends one query per application server and shares the result.
- The cost of RAM. This is a budget problem, not an outage. Moving less used data onto flash drives keeps the cost under control.
If the interviewer pushes
- "Why single threaded when we have many cores?" Even a read changes the eviction list, so many threads would need locks, and the locks would cost more than the memory work itself. To use the other cores, run several processes per machine. Redis adds I/O threads for the network, but its commands still run on one thread.
- "What happens on a node failure?" The cluster manager notices the missed heartbeats, promotes a replica for each range the node owned, and sends a new map to the clients. Writes that had not reached a replica are lost, which is why the database stays the source of truth.
- "How do expired keys actually get removed?" Two ways. Lazily, when someone reads them, and by a background task that samples 20 random keys with a TTL, 10 times a second.
- "Is this how Redis Cluster works?" Mostly. Redis Cluster uses 16,384 fixed hash slots instead of a ring, and its nodes vote on failover among themselves instead of using a separate manager.
- "When is this the wrong tool?" When you cannot accept losing a write. This design gives up durability for speed on purpose.
Reading Progress
0%
On This Page
1. Problem Definition and Scope
2. Clarify functional requirements
3. Clarify non-functional requirements
4. Back of the envelope estimates
5. API design
6. High level architecture
7. Data model
8. Core flows end to end
Flow 1: Saving Data (The "SET" Request)
Flow 2: Retrieving Data (The "GET" Request)
Flow 3: The Cache Miss (Reading from the Database)
9. Caching and read performance
10. Storage, indexing and media
11. Scaling strategies
12. Reliability, failure handling and backpressure
13. Security, privacy and abuse
14. Bottlenecks and next steps
The complete picture
Summary
The decisions that mattered
The numbers that forced them
What breaks first
If the interviewer pushes