SysDesignPrep.com
System design interview question

Design a Distributed Cache

A Redis-like cache as a service: hundreds of nodes, terabytes of memory, sub-millisecond gets, and a cluster that survives losing a machine mid-request.

Last updated 2026-09-22. Difficulty: hard. Patterns: consistent-hashing, eviction, replication, hot-keys. Reported at Anthropic and 6 more with Pro.

Walk through a strong candidate's answer, turn by turn.

The interviewer asks, the candidate answers and draws, and you press Next. Pause to answer yourself at the key decisions, and ask the coach anything along the way.

Functional requirements

  • Get, set and delete a key. Values are opaque bytes up to a few hundred KB. Set takes an optional TTL. This is the whole hot path and it must be one network round trip.
  • Expiry by TTL. A key disappears at its deadline whether or not anyone reads it. Lazy expiry on read plus background sampling, because scanning every key is not affordable.
  • Eviction under memory pressure. When the node is full, admit the new key by evicting others. Policy is per namespace: LRU by default, LFU for skewed read patterns, or reject writes for a cache that must not lose data.
  • Atomic numeric and structural operations. Increment, compare-and-set, list and set operations. These are why teams pick a cache with data structures over a plain memcached; they also keep counters correct without a read-modify-write race.
  • Namespaces with independent limits. One cluster serves many teams. Each namespace has a memory quota, an eviction policy, a default TTL and its own rate limit so one tenant cannot evict another out of memory.
  • Online resharding. Add or remove nodes without downtime and without invalidating the whole keyspace. Slots migrate; clients follow redirects while a migration is in flight.
  • Replication and failover. Each shard has a primary and at least one replica in another availability zone. A primary failure promotes a replica in seconds without operator involvement.
  • Observability per key class. Hit rate, evictions, memory, p99 latency and hot keys, broken down by namespace and key prefix, because "the cache is slow" is otherwise undebuggable.
  • Out of scope. Durable storage semantics (this is a cache, not a database), cross-region active-active writes, and a query language over cached values.

Non-functional requirements

  • Latency (p99 < 1 ms in-zone). A cache that is sometimes slow is worse than no cache, because callers have already paid for the lookup. Single-digit milliseconds at p999 including a retry.
  • Throughput (10 M ops/s per cluster). Driven by a fleet of application servers, each doing thousands of gets per second. Per-node ceiling is what sets the cluster size.
  • Capacity (10 TB of hot data). Memory is the expensive resource; the design lives or dies on bytes per entry and on eviction picking the right victims.
  • Availability (99.99 % for reads). The cache is on the critical path of every product. Losing it must degrade to a slower origin, never to an outage, which pushes work onto the client library.
  • Consistency (best-effort, bounded staleness). A cache may return stale data and may lose data on failover. This is the requirement that lets the rest of the design be fast, and it must be stated out loud.
  • Durability (none guaranteed). A cold start after losing a whole cluster must not take the origin down; warming is part of the design rather than an afterthought.
  • Isolation (no cross-tenant blast radius). A tenant that writes 2 GB/s or fills memory with 10 MB values must be throttled, not allowed to evict everyone else.
  • Elasticity (resize in minutes, no cold cache). Traffic doubles for a sale; adding nodes must not invalidate the existing keys, which is the argument for consistent hashing over modulo.

Back-of-envelope estimates

  • Operations per second: ~10 M. 5 000 application servers × 2 000 cache ops/s each = 10 M ops/s. Reads are roughly 20:1 over writes, which is what makes replicas useful for read offload.
  • Nodes needed for throughput: ~60–100. A single-threaded Redis-style process on modern hardware handles ~150 k ops/s with pipelining and small values; budget 120 k to leave headroom. 10 M ÷ 120 k ≈ 84 primaries, round to 100 for growth.
  • Memory for the working set: ~13 TB usable. 10 TB of values plus per-entry overhead: key (~40 B) + value header, TTL and LRU metadata (~50–100 B) + allocator fragmentation (~10–20 %). 10 TB × 1.3 ≈ 13 TB of RAM before replication.
  • Machines for memory: ~90 primaries. 13 TB ÷ ~144 GB of usable cache memory per machine (a 192 GB box, leaving headroom for replication buffers and a fork) ≈ 90. With one replica each, ~180 machines. Memory, not CPU, is the binding constraint at this value size.
  • Network per node: ~1.2 Gbps. 120 k ops/s × ~1.2 KB average value ≈ 144 MB/s ≈ 1.2 Gbps on a 10–25 Gbps NIC. Fine, until someone caches 1 MB objects: the same op rate would need 120 GB/s, which is why per-value size limits exist.
  • Hit rate and origin load: 95 % → 500 k/s to origin. At 10 M ops/s with a 95 % hit rate the origin databases absorb 500 k/s. Moving to 99 % cuts that to 100 k/s. Every point of hit rate is worth five times its weight in database capacity, which is the argument for spending on memory.
  • Cold start cost: ~20 min at safe refill. Refilling 10 TB from origin at a rate the databases survive (say 200 k fills/s at 1.2 KB = 240 MB/s) takes 10 TB ÷ 240 MB/s ≈ 12 h for everything, but only the hot 5 % matters, so ~20 minutes to get back above a 90 % hit rate. The lesson: never restart a whole cluster at once.
  • Failover impact: ~1 % of keys, seconds. One primary of 100 holds ~1 % of the keyspace. A failure loses whatever was not replicated (milliseconds of writes with async replication) and makes that slice miss for the 5–15 s of detection and promotion. Expressed as origin load: a 5 % bump for a few seconds.

Components

  • Application (smart client library): Runs the cache-aside pattern and holds the cluster topology in memory, so a get is one hop to the right node rather than a proxy round trip. Owns the tight timeouts, the single retry, the circuit breaker and the fallback to origin. Also does client-side batching and, for the very hottest keys, a small in-process cache.
  • Cache proxy (optional · thin routing tier): For clients that cannot embed a smart library (other languages, serverless, external teams). Terminates connections, keeps pools warm to every node, and does the same slot routing. Costs a hop of latency, saves a connection explosion: 5 000 clients × 100 nodes is 500 k connections without it.
  • Cache node (in-memory · single-threaded core): Owns a set of hash slots. Keeps an in-memory hash table with per-entry TTL and LRU/LFU metadata, serves gets and sets from the event loop, and handles its own expiry sampling and eviction. Rejects values over the size limit and tracks per-namespace memory so one tenant cannot consume the node.
  • Replica (async replication · other AZ): Streams writes from its primary and can serve reads for clients that accept staleness. Exists mainly so a machine loss costs seconds rather than a cold slice, and so a planned restart is a promotion rather than an outage.
  • Topology store (etcd / ZooKeeper · slot map): The small strongly consistent store of truth: slot → primary, primary → replicas, migration state and epoch. Read on client start and watched for changes; never on the request path. A few kilobytes of data protected by consensus.
  • Control plane (placement · resharding): Decides where slots live. Runs cluster creation, scale up and down, slot migration, rebalancing after failure, and rolling upgrades one node at a time. Every action is a change to the slot map plus orchestration of the nodes involved, so the data path stays simple.
  • Failure detector (gossip + quorum): Nodes gossip liveness; a primary is declared dead only when a quorum of peers agrees and the control plane confirms. Deliberately conservative: a false positive causes an unnecessary failover and a small data loss, so detection is tuned for a few seconds rather than a few hundred milliseconds.
  • Origin store (the database being protected): The source of truth behind the cache. The design exists to keep this from being hit 10 M times a second; every failure mode is judged by how much load it pushes here.
  • Warmer (top-key replay): Rebuilds the hot set after a cold start. Consumes the sampled key log, replays the most frequent keys against the origin at a rate the origin can absorb, and reports hit rate as it climbs. The reason a restart is not an incident.
  • Key sample stream (sampled · 1 in 1000): A 0.1 % sample of operations emitted by the nodes: key prefix, namespace, size, hit or miss. Feeds hot-key detection, working-set estimation and the warmer. Sampling keeps this at a few thousand events a second instead of ten million.
  • Hot-key detector (count-min sketch): Maintains approximate top-K per namespace from the sample stream. When a key exceeds a threshold share of a node, it publishes it to clients, which then cache it locally for a second or two, and to the control plane, which can replicate that slot more widely.
  • Metrics & quotas (per namespace): Aggregates hit rate, memory, evictions, expirations, latency percentiles and bandwidth per namespace and key prefix. Enforces quotas by telling nodes to throttle or reject a namespace that is over its memory or bandwidth budget, which is what keeps tenants isolated.
  • Dashboards & alerts (on-call view): Where hit-rate regressions, eviction storms, memory fragmentation and failovers surface. The metric people page on is client-observed latency and error rate, not node CPU.

User flows

  1. A cache hit, and a miss that fills. The path that runs ten million times a second. What matters is that a hit is one hop with no coordination, and that a miss cannot turn into a stampede.
    1. Client hashes the key to a slot and picks the node that owns it from its local topology map. CRC16 of the key modulo 16 384 slots. Slots, not keys, are the unit of placement: the map is 16 384 entries instead of billions, and moving data means moving slots. Hash tags ({user:123}:profile) let related keys land together so multi-key operations stay on one node.
    2. Client sends the get on a pooled connection and waits with a tight timeout. Connections are long-lived and pipelined; a new TCP handshake per operation would cost more than the lookup. The timeout is a few milliseconds, because the whole point of the cache is that slow is failed.
    3. Node looks the key up in its hash table, checks the TTL, and updates the recency metadata. Expiry is checked lazily on read: an entry past its deadline is deleted and reported as a miss, so no reader ever sees an expired value even though nothing scanned it. The access also touches the LRU clock or the LFU counter, which is what eviction will use later.
    4. On a miss, the client reads the origin under a per-key lock so only one caller does the work. The stampede guard: a short-lived lock key (SET NX with a 5 s TTL) means one request fills while the others wait briefly or serve stale. Without it, a popular key expiring turns 50 k requests a second onto one database row.
    5. Client writes the value back with a jittered TTL, and the node admits or evicts to make room. TTL jitter of ±10 % stops keys written together from expiring together. If the namespace is at its memory limit, the node evicts according to policy before storing.
    6. Callers that cannot embed the library send the same operation through the proxy tier instead. One extra hop of a few hundred microseconds, in exchange for pooled connections and a routing map the caller does not have to understand. Serverless functions and other languages use this path; the routing logic is identical, so the two paths cannot disagree.
    7. Node samples the operation into the key stream for hot-key and working-set analysis. One in a thousand operations, with the key prefix rather than the full key where the key contains user data. This is how the system knows its hit rate per prefix and which keys are hot, without paying to log ten million events a second.
  2. A write and its invalidation. Keeping a cache close to correct is mostly about what the writer does. The chosen pattern is write-through to the database and delete the key, with a version to block a stale fill.
    1. Application writes the new value to the origin database in a transaction. The database remains the source of truth. Nothing about the cache is allowed to change the fact that a committed write is durable.
    2. Application deletes the cache key rather than overwriting it. Delete, not set: two concurrent writers that each set their own value can leave the losing value behind forever, while delete leaves only a miss, which repairs itself. The cost is one extra origin read on the next request.
    3. The next reader misses, reads the origin, and stores the value with its version. Storing the version inside the cached value makes the classic race detectable: a slow reader that fetched before the write can only overwrite with an older version, and the node rejects that with a compare-and-set.
    4. Node replicates the mutation to its replica asynchronously. Asynchronous because a synchronous replication round trip would double the write latency for data that is, by definition, reconstructible. The window of loss is sub-millisecond and the consequence is a miss.
    5. A delayed second delete guards against a fill that raced the write. Delayed double delete: schedule another DEL a second later for the rare interleaving where a reader fetched an old row just before the write and stores it just after the first delete. Cheap insurance on the few key classes where a stale read is expensive.
  3. Add nodes without losing the cache. Scaling up must not invalidate the keyspace. Slots move one at a time, and clients are redirected mid-migration rather than being told to stop.
    1. Operator asks the control plane to grow the cluster; it computes the slots to move. Going from 90 to 100 primaries moves roughly a tenth of the slots. With modulo hashing nearly every key would move; with slots plus consistent hashing only the arcs assigned to the new nodes do.
    2. Control plane marks a slot as migrating: source is MIGRATING, destination is IMPORTING. Both nodes now know about the migration, which is what makes the next step safe. Only one slot is in flight at a time per node pair, so the blast radius of a failure is one slot.
    3. Keys in the slot are copied in batches while the source continues serving reads and writes. Each batch is copied atomically and deleted from the source, so a key exists in exactly one place. The slot stays available throughout; only the individual keys in flight are briefly locked.
    4. A client that asks the source for a key already moved is redirected to the destination. This is why migration needs no client downtime. A MOVED response updates the client topology permanently; an ASK response redirects only this one request, because the slot is still migrating. Clients that ignore redirects see misses, not errors.
    5. When the slot is empty, the control plane commits the new owner into the topology store and bumps the epoch. The epoch is how a client or node with an old map recognises that it is stale. Watching clients pick up the change within a second; the rest learn on their next redirect.
    6. Migration continues slot by slot with a rate limit while the metrics show hit rate holding. The safety check is hit rate and client-observed latency, not migration speed. If either degrades, the plan pauses: a resharding that halves the hit rate has pushed the difference onto the origin database.
  4. A primary dies mid-traffic. The failure case that decides whether the cache is safe to depend on. Detection is deliberately slow, promotion is fast, and the client absorbs the gap.
    1. Peers stop receiving gossip from the node and mark it suspect. A single missed heartbeat means nothing: a garbage collection pause, a NIC blip and a dead machine look identical for the first second. Suspicion requires several missed intervals, typically a few seconds in total.
    2. A quorum of peers agrees the node is down and the failure detector tells the control plane. Quorum, not one observer, because a partitioned observer would otherwise trigger a failover and create two primaries. This is the split-brain guard, and the epoch on the slot map is the second one.
    3. Meanwhile clients time out, retry once against the replica, and then treat the key as a miss. This is what keeps the failure invisible to users: the client library degrades to the origin instead of failing the request. Origin load rises by the share of traffic on that node, which is why 1 % slices matter.
    4. Control plane promotes the replica, assigning it the failed primary's slots with a new epoch. The promoted replica holds everything that was replicated before the failure, so most of the slice is still warm. Writes that were in flight are lost, which is acceptable for a cache and must be stated as a property, not discovered in an incident.
    5. Clients watching the topology store switch within a second; stragglers learn through MOVED. Two paths to the same outcome: push for the fast case, redirect for the ones that missed the push. A client with a stale map is never wrong for long, and never writes to the old primary because it fences on the epoch.
    6. A fresh replica is built for the new primary in the background, rate limited so it does not disturb reads. Until it finishes, the shard is a single point of failure, so this is the step the on-call engineer watches. Building a replica streams the keyspace, which is bandwidth the primary is also using to serve traffic.
    7. The warmer refills whatever the promoted node is missing, replaying the top keys against the origin at a safe rate. The sampled key log knows which keys were hot on that slice, so refilling is ordered by popularity rather than random. The fill rate is capped by what the origin can absorb, which is the whole point: recovery must not become a second outage.
  5. One key takes a whole node down. The failure mode unique to caches: a single key so hot that its owning node saturates while the rest of the cluster is idle. Sharding cannot help, because the key is indivisible.
    1. A product launch makes one key the target of 2 M requests per second. Consistent hashing spreads keys, not requests for one key. The owning node is now at 2 M ops/s against a 150 k ceiling: latency climbs for every key it owns, not just the hot one.
    2. Node latency and bandwidth for that namespace spike, and the alert fires before anyone files a ticket. The alert is on client-observed p99 and per-namespace bandwidth, not on node CPU: a single-threaded cache node saturates on one core long before the machine looks busy, so CPU utilisation is the metric that hides this failure.
    3. The sample stream shows one prefix dominating; the detector flags it as hot. A count-min sketch over the sampled stream gives an approximate top-K per namespace in a few kilobytes. Approximate is fine: the decision is "is this key more than a few percent of a node", and the answer is obvious when it happens.
    4. The hot key is published to clients, which cache it in process for a second or two. This is the fix that actually works: 5 000 clients each holding the value for 1 s turns 2 M remote gets per second into 5 000. The cost is up to a second of extra staleness on that key, which is a product decision, and the mechanism is why the client library exists at all.
    5. For keys that cannot tolerate local staleness, the control plane replicates the slot more widely and clients read from replicas. Adding read replicas multiplies read capacity for that slot at the cost of memory and replication bandwidth. It is slower to take effect than local caching, so it is the second lever, not the first.
    6. The client library also spreads the key across N aliases when writes are rare. Key splitting: store the same value under product:hero#0 … #9 and have each client read a random alias, so the load lands on up to ten different slots. Writes must update all copies, which is why this is reserved for read-mostly values.
    7. Quotas throttle the namespace if it is still saturating the node, protecting the other tenants. The last line of defence. A tenant over its bandwidth budget gets errors that its own client library degrades on, rather than being allowed to make the cache slow for everyone. Isolation beats fairness during an incident.

Deep dives

Client-side routing or a proxy tier

How does a request find the node that owns its key: a smart client, a proxy, or something in the middle?

Every operation has to be routed to exactly one node, and the routing decision is on the critical path of a sub-millisecond operation.

The cluster changes shape: nodes fail, slots migrate, capacity grows. Whatever holds the map has to learn about that quickly without consulting a central service per request.

At 5 000 clients and 100 nodes, a fully connected mesh is 500 000 TCP connections, which is itself a design constraint.

  • Smart client with a cached slot map chosen
  • Proxy tier (twemproxy, envoy-style) situational: Offered alongside the smart client for languages without a good library, for serverless callers, and for external teams. Both paths route identically.
  • Server-side redirection only rejected
  • Consistent hashing in the client with no slot map rejected

The answer: A smart client holding a 16 384-entry slot map, watched from the topology store and corrected by MOVED/ASK redirects, with an optional thin proxy for clients that cannot embed the library.

What happens when a client has a stale slot map during a migration?

It asks the old owner, which answers ASK (this request only, slot still moving) or MOVED (slot has moved, update your map). ASK is followed for a single request and does not change the map; MOVED updates it. Either way the operation succeeds on the first retry, so staleness costs latency rather than correctness.

Why 16 384 slots rather than hashing keys directly to nodes?

Slots decouple the key space from the node count. The map is small enough to ship to every client and to gossip as a bitmap, placement decisions are made over a few thousand units instead of billions of keys, and a migration is a well-defined "move slot 4211" operation with a clear start and end.

How do you avoid 500 k connections?

Long-lived pooled connections with a small pool per node per client process (2–4), multiplexing and pipelining on each, plus the proxy tier for fleets where that is still too many. Connection count is a first-class capacity number: each one costs memory and a file descriptor on the node.

A client library bug sends 10x traffic. What limits the damage?

Per-namespace quotas enforced at the node, per-connection rate limits, and a circuit breaker inside the library itself. The node also has a hard cap on concurrent clients and will reject rather than queue unboundedly, since a queued request at a 5 ms timeout is already useless.

Choosing what to throw away

When memory is full, which key is evicted, and how is that decided without scanning everything?

Memory is the expensive resource and it is always full; a cache that is not evicting is a cache with too much memory.

Exact LRU needs a linked list touched on every read, which costs memory per entry and hurts cache locality.

Access patterns differ per namespace: session data is recency-driven, a product catalogue is frequency-driven, and a precomputed feed is neither.

  • Approximate LRU by sampling chosen
  • Approximate LFU with decay situational: The default for read-heavy namespaces with a stable popularity distribution, such as catalogues and feeds; selectable per namespace.
  • TTL-only, no eviction (reject writes when full) situational: Namespaces where losing an entry is a correctness problem rather than a performance one, for example distributed locks or in-flight payment idempotency records.
  • Exact LRU with a linked list rejected

The answer: Approximate LRU by sampling as the default, LFU for read-heavy skewed namespaces, and no-eviction for namespaces whose entries are correctness-relevant, with the policy and memory quota set per namespace.

How does expiry work without scanning the keyspace?

Two mechanisms together: lazy expiry deletes an expired key when it is read, so no reader ever sees one; and a background loop samples a small number of keys with TTLs every cycle, deletes the expired ones, and repeats more aggressively while the observed expired fraction is high. Memory is therefore reclaimed proportional to how much has actually expired.

A batch job scans a million cold keys and evicts the hot set. What stops that?

That is exactly what LFU prevents, because a single access never promotes a key above entries with high counters. Additional defences: the batch uses its own namespace with its own quota, and clients can pass a hint that marks reads as non-promoting.

How do you size memory so evictions are rare but memory is not wasted?

From the working set, not the total data: sample the key stream, estimate the number of distinct keys touched in a window, multiply by average entry size plus overhead, and add headroom. Then watch the eviction rate and the hit rate together: evictions climbing while hit rate holds means you are evicting genuinely cold data, which is healthy.

What about memory fragmentation?

Allocators with size classes leave gaps when values vary in size, so resident memory can exceed the data by 20–50 % in the worst cases. Mitigations: keep values within a few size classes where possible, cap the maximum value size, monitor the fragmentation ratio, and use active defragmentation or a rolling restart of the replica-then-primary when it gets bad.

What the cache promises about staleness

How is the cache kept consistent enough with the origin, and what exactly is promised to callers?

Every cached value is a copy, so the only question is how stale it may be and what happens when a write races a fill.

The classic race: reader misses, reads the origin, and stores the value after a concurrent writer has already invalidated, leaving a stale value with a full TTL.

Different data has genuinely different tolerance: a display name can be a minute stale, a permission check after a revoke cannot.

  • Write to origin, delete the key, version the stored value chosen
  • Write-through: update cache and origin together rejected
  • Invalidate from the database change stream (CDC) situational: Added as a safety net for high-value key classes and for origins written by many services; belt and braces rather than the primary mechanism.
  • Short TTLs and accept staleness situational: Fine for data with no correctness requirement at all: trending lists, aggregate counts, recommendation slates.

The answer: Write the origin, delete the key, and store a version with every cached value so a slow fill cannot overwrite a newer one, with TTL jitter on every entry and a CDC-driven invalidation stream as a safety net on the key classes that matter.

Walk me through the stale-fill race in detail.

Reader R misses and reads version 41 from the origin. Writer W commits version 42 and deletes the key. R then stores version 41 with a ten-minute TTL, and every subsequent reader sees stale data until it expires. The version check fixes it: R's store is conditional on the cached version being lower than 41, and once 42 has been stored the write is refused. The delayed double delete covers the case where nothing has been stored yet.

When would you not cache at all?

When a stale read causes a wrong action: an inventory count at checkout, a balance before a transfer, a permission check immediately after a revoke. For those, read the primary, or cache with a TTL measured in a few seconds and accept the origin load. Saying which data is deliberately uncached is part of the answer.

How do you give a user read-your-writes without slowing everyone down?

Pin that user briefly: after their write, either keep their new value in their session, or mark their key with a short "recently written" marker so their own reads bypass the cache for a few seconds. It is a per-user cost rather than a global consistency guarantee.

The cache and the origin disagree and nobody knows why. How do you debug it?

A sampled comparison job: read a small number of keys from both and report mismatches by key prefix. Combined with the version stored in each value, it tells you whether the problem is a missed invalidation (cached version older than origin) or a stale fill (version older than a write you can find in the log). Without the version in the value, this is guesswork.

Replication, failover and what a cache may lose

Should a cache replicate at all, and if so, synchronously or asynchronously?

A cache is reconstructible by definition, so replication buys availability and warm failover rather than durability.

The cost is real: a replica per primary doubles the memory bill, which at 13 TB is the largest line item in the design.

Losing a primary without a replica means a cold slice: every key on it misses until refilled, which is a load spike on the origin.

  • Asynchronous replica per primary, cross-AZ chosen
  • No replication, rely on refill situational: Reasonable for a small cache in front of a database that can absorb the miss storm, or for caches where every key is cheap to recompute.
  • Synchronous replication rejected
  • Persist to disk (AOF/snapshot) instead of replicating situational: Useful for planned restarts and upgrades on namespaces that are expensive to refill; never the primary availability mechanism.

The answer: One asynchronous replica per primary in a different availability zone, with quorum-based failure detection, epoch-fenced promotion, and an explicit written promise that a failover may lose the last few milliseconds of writes.

How do you avoid two primaries after a partition?

Promotion requires a quorum of peers to agree the primary is gone and is recorded in the consensus-backed topology store with a higher epoch. A returning old primary sees the newer epoch and demotes itself, and clients fence on the epoch so a write sent to the old primary is rejected. Heartbeats alone cannot prevent split brain; the epoch and the quorum do.

Why is detection deliberately slow?

Because a 2-second GC pause, a brief network drop and a dead machine look identical at first. Failing over on a false positive costs a real data loss window and a load spike, so the detector waits several missed intervals and requires agreement. The client absorbs the gap by degrading to the origin, which is cheaper than an unnecessary failover.

Can replicas serve reads?

Yes, for callers that accept staleness of a few milliseconds, and it is the second lever for a hot slot. The catch is that a caller reading its own write from a replica may not see it, so the client library only routes to replicas for key classes marked stale-tolerant.

What does a rolling upgrade look like?

Upgrade the replica, verify it is in sync, promote it, upgrade the old primary as the new replica, and move on one shard at a time. The failover is planned rather than triggered by failure, so the write-loss window is controlled, and the cluster never has more than one shard in a degraded state.

Cold start and cache stampedes

The cache is empty, or a hot key just expired. How do you stop the origin from falling over?

A cache in front of a database is load-bearing: at a 95 % hit rate, an empty cache means twenty times the database traffic.

The two versions of the problem are the same shape: many requests for something the cache cannot answer, all arriving at once.

Any mitigation has to work with the cache unavailable, not only when it is merely empty.

  • Per-key locking plus stale-while-revalidate chosen
  • Probabilistic early expiration situational: Used together with locking for the small set of very hot keys where even a single miss is a visible spike.
  • Warmer job replaying top keys after a cold start chosen
  • Origin rate limiting and load shedding chosen

The answer: All three layers: locking with stale-while-revalidate in the client for the steady state, a warmer driven by the sampled key log for cold starts, and origin-side limits with a degraded response so the worst case is slow rather than down.

How do you restart a hundred-node cluster safely?

Never all at once. Restart is a rolling promotion (upgrade the replica, promote it, restart the old primary) so at most one shard is cold at a time and the cluster-wide hit rate barely moves. A genuine full cold start is handled by the warmer with the origin rate-limited, and the order is by descending key popularity.

What is stale-while-revalidate here, exactly?

Each entry carries a soft TTL and a hard TTL. After the soft TTL the value is still served, but one caller takes the lock and refreshes it in the background. Only after the hard TTL is the value treated as gone. Users see a slightly stale value instead of an origin round trip, and the origin sees one request instead of thousands.

A key genuinely does not exist and something probes it a million times. What happens?

Negative caching: store a tombstone with a short TTL, 5 to 30 seconds, so the miss is answered by the cache. The short TTL matters because a record created moments later must not be hidden. A Bloom filter of existing keys is the cheaper option when the key space is enumerable.

How do you test that any of this works?

Deliberately, in production, during working hours: kill a node and watch origin load and client latency; flush a namespace on a canary and watch the warmer climb; and run a load test against the origin with the cache disabled to find out what its real ceiling is. A degradation path that has never been exercised is a guess.

Many teams, one cluster

How do you stop one team from ruining the cache for everyone, without giving every team its own cluster?

A hundred small caches waste memory through fragmentation and are an operational burden; one big cache shares fate across every team.

The failure modes are specific: a tenant that writes huge values, a tenant that fills memory and evicts everyone else, and a tenant whose traffic saturates a node.

Chargeback matters too: someone has to see which namespace is consuming the 13 TB.

  • Namespaced quotas on one shared cluster chosen
  • A dedicated cluster per team situational: Offered to tenants with strict latency requirements, regulated data, or a size that justifies their own fleet.
  • Cells: a handful of shared clusters, tenants assigned to one chosen
  • No isolation, rely on good behaviour rejected

The answer: A small number of cells, each a shared cluster with per-namespace memory quotas, value-size limits, bandwidth limits and eviction policy, plus dedicated clusters for the few tenants that genuinely need them.

How is a memory quota enforced without a scan?

The node tracks bytes per namespace incrementally on every set, delete, eviction and expiry. When a namespace is over quota, its writes evict only from its own entries (so it cannot push another tenant out of memory) and if its policy is no-eviction, its writes are rejected instead.

A tenant caches 5 MB values. What breaks and what do you do?

Bandwidth and tail latency break first: a few hundred of those per second saturate the NIC and every other operation on that node queues behind them. The fix is a hard per-value size limit (say 1 MB) enforced at the node with a clear error, plus a suggestion to store the blob in object storage and cache the key. Large values belong in a different system.

How do you charge teams for what they use?

Bill on the peak memory footprint and the bandwidth per namespace, both of which the node already tracks for quotas. Publishing those numbers changes behaviour more than any technical control: teams find their own 200 GB of keys with no TTL once they can see them.

One namespace needs strong isolation for latency. What do you offer?

Either a dedicated cell, or dedicated nodes within a cell by pinning its slots to machines that serve no other namespace. The second option keeps one control plane and one upgrade path, which is usually the better trade for everyone involved.

Related