Cruxly

Cruxly › System design › Scaling Building Blocks

Cache Eviction and Topology

which key to evict, and which machine owns it

Open the interactive lesson

The previous lesson covered when to cache and how fresh it should be. This one covers the two questions that come right after: when the cache is full, what gets thrown away — and once the cache is bigger than one machine, which machine holds which key.

Eviction is a bet on the next request

A cache is finite, so something has to leave when it fills. Every eviction policy is a bet on which key you're least likely to need next.

PolicyEvictsHow it fails
LRU (least recently used), the default: traffic clusters in timeThe key untouched longestCan't tell "read a minute ago, likely again" from "read once, never again". A one-time scan makes every scanned key most recently used and evicts the whole hot working set
LFU (least frequently used)The key with the fewest accesses everScan-proof: a one-time read gets a count of 1, never enough to displace a key read thousands of times. But a key hot last week keeps its count and never leaves, unless counts decay
ARC / window-TinyLFUWeighs recency and frequency, shifting to whichever predicted hits better recentlyMore bookkeeping. What Caffeine and some Redis-adjacent designs actually run

LRU's implementation (hash map + doubly linked list) is in the LRU cache exercise in this track. In an interview, "adaptive between LRU and LFU, weighted by recent hit-rate evidence" beats defending one side as permanent.

:::warn "I'd use LRU" alone is not a complete answer Name the access pattern, then the policy, then the failure you accept: "Recency-clustered traffic with occasional batch scans, so plain LRU risks the scan problem. I'd use window-TinyLFU, or send scan traffic to a separate, smaller cache so it can't evict the real working set." :::

If asked "what about FIFO?", place each in one sentence. Ruling one out correctly is itself a signal.

PolicyEvictsVerdict
FIFOThe oldest insertArrival order, not access order: drops hot data as readily as cold. A queue policy, not a cache policy
LIFO / FILOThe newest insertWorse: evicts what you were most likely to read next
MRU (most recently used)Whatever was just touchedCorrect only for a known one-pass scan (a database reading a large table once through a small buffer), where "just touched" means "cold forever". A narrow exception
RandomA random keyNo bet, so no access pattern can be crafted to defeat it. Cheap: Redis's approximated LRU samples a few keys instead of tracking exact recency

One machine is not enough: partitioning the cache itself

Once the working set outgrows one machine's memory, or the request rate outgrows its throughput, the cache itself must be partitioned across nodes.

:::key Same mechanism as database sharding, different stakes Both use consistent hashing. But a cache miss is cheap (fall through to the database); a database read on the wrong shard is a correctness bug. :::

Two separate decisions, not alternatives:

  1. The mapping (key → node): plain % N (avoid), a consistent-hashing ring, rendezvous hashing, or fixed hash slots (Redis Cluster: 16,384 slots, each owned by one node; rebalancing moves whole slots).
  2. Where the mapping runs:
Where it runsLatencyThe cost
Smart client (e.g., Redis Cluster clients)No extra hop: the client caches the slot map and talks straight to the ownerEvery client library must get it right; during resharding the server replies MOVED/ASK and the client updates its map
Proxy layer (Twemproxy, Envoy)One more hopNo service reimplements the mapping, but the proxy is another tier to scale and operate

:::warn Plain % N remap on node-count change is a full cache flush Changing N changes key % N for nearly every key. Add the cold-cache problem from the previous lesson and "add a cache node" can briefly take your database down. Consistent hashing remaps only about 1/N of keys. :::

Replicate the cache tier? The data is disposable (the database holds the real copy), so no durability story is needed. Availability is the question: if the database was sized assuming the cache absorbs most reads, losing one node sends its share, often the hottest keys, straight to the database.

Your caseDo this
The database can absorb one cache node's miss trafficSkip replication; accept losing a node
It can't (the cache is load-bearing)Cache replicas, or a gutter pool (Facebook): a few spare cache servers take over a failed node's keys with short TTLs, so its traffic doesn't land on the database

Redis or Memcached: choosing the shared tier

Everything above assumes a shared tier exists. Two engines fill it, and "which one" is a question about what you will ask of the tier besides GET and SET. (The partitioning mechanics are in Distributed Cache Cluster; this section is about the engines underneath.)

DimensionMemcachedRedis
ModelStrings under a key, nothing elseStrings plus lists, sets, sorted sets, hashes, bitmaps and more, with server-side commands on each
ThreadsMultithreaded: each worker thread runs its own event loop, so one big node uses many coresCommands run mostly on one thread. Optional I/O threads (io-threads, off by default, reworked in Redis 8) take over socket reads and writes, but the docs still advise several instances to use several cores
DurabilityNone by default. A restart is an empty cacheOptional RDB snapshots and an append-only file
ReplicationNone built in; servers do not talk to each otherAsynchronous primary-replica copies
ShardingClient-side hashing across independent serversRedis Cluster, or client-side as well
MemorySlab classes: fixed-size chunks per class, one LRU per classPer-key overhead grows with richer types; eviction is sampled

What the data structures buy. A sorted set holds a leaderboard that is updated and ranked inside the server; with a plain key/value tier you would read the whole blob, change it and write it back, racing every other writer. That is the real argument for Redis: logic that must be atomic and server-side. If every value is an opaque serialized object that you fetch and replace whole, the richer types buy nothing.

What durability and replication change. A cache tier is disposable, so for pure caching their main use is a smaller stampede after a restart: a Redis node that reloads its snapshot comes back warm; a Memcached node comes back empty and sends its whole share to the database. Do not over-read them. Redis replication is asynchronous, and WAIT makes a write more likely to survive but does not make Redis a strongly consistent store. Redis Cluster can lose acknowledged writes in a partition. A tier you treat as a source of truth needs a design for that, not a flag.

Eviction in practice. Memcached evicts by LRU inside each slab class, so a workload whose item sizes shift can starve one class while another sits idle; Facebook wrote an adaptive allocator that moves slabs to the class that is evicting recently used items. Redis lets you pick the policy and approximates LRU by sampling a few keys. With noeviction it refuses writes when full: correct for a datastore, wrong for a cache. Policies, slots and hash tags (multi-key operations need keys in one slot, and a hot tag is a hot node) are covered in Redis in depth: cluster slots, eviction, persistence and the lock debate.

A choice rule.

Why teams still pick Memcached at scale. Facebook's Memcache deployment is the reference: a demand-filled look-aside cache where writers delete the key rather than update it, with the engine kept simple and the intelligence moved into a client library and a proxy (mcrouter). Their leases are the engine-level answer to stale sets and herds: a miss hands one client a token to refill the key, issued at most once per 10 seconds per key, and the others wait briefly. For one set of herd-prone keys, peak database load fell from 17K to 1.3K queries per second. That design is possible because the engine is simple; it is not an argument that the simple engine is always right.

:::key Verified November 2026

:::

Worked arithmetic. A 10-node Memcached tier with consistent hashing loses 10% of the cached keys when a node dies. If the tier absorbs 95% of 100,000 reads per second, the database normally sees 5,000; during the loss it sees about 5,000 + 0.10 x 95,000 = 14,500, nearly three times its usual load, until the keys refill. A replica ready to promote would largely remove that spike. Whether it is worth paying for depends on whether the database can take 14,500.

Further reading: Nishtala et al., Scaling Memcache at Facebook (NSDI 2013), and the Redis and Memcached documentation.

Proxies: the layer that sits in front and does more than route

A proxy (Twemproxy or similar) sits between client and origin. Beyond picking a cache node, it can act on every request and response without either side knowing.

JobExamples
FilterAccess checks; reject malformed requests before they reach the origin
LogRecord every request centrally, without instrumenting each origin
TransformAdd or strip headers; TLS termination (the most common case); compress responses for slow clients

Collapsed forwarding is the technique worth naming: several requests for data that sits close together (nearby map tiles, adjacent rows of an uncached range) become one origin request, and that response answers every caller. It's request coalescing (from the thundering-herd material) generalized from "the exact same key" to "keys cheap to fetch together". The proxy is the place for it because it's the one component that sees all the requests.

Coherence across a fleet, and the two-hop invalidation bug

The previous lesson assumed one cache tier. Many systems cache the hottest keys twice: in the shared tier, and in a small in-memory layer per app instance so the hottest reads never leave the process. Invalidating the shared tier doesn't reach those local copies.

A two-tier cache has two invalidation problems, not one. Three fixes, none free:

FixFreshnessThe cost
Very short local TTLs (seconds)Stale for at most the local TTLA small staleness window everywhere, always
Publish invalidation events (Redis pub/sub or a queue topic); each instance evicts the key on arrivalNear-immediateAn instance that misses the event (network blip, slow consumer) stays silently stale. Keep a backstop TTL underneath
No local tierOnly the shared tier'sA network hop on every read. A legitimate answer when the local tier's correctness risk outweighs the latency it saves

Every extra tier is one more place staleness can hide, and one more invalidation path to exercise in practice, not just draw on a whiteboard.

How to say it in the interview

"Eviction policy is a bet about what the next request looks like — LRU bets on recency, LFU bets on frequency, and the wrong bet for this workload is a real performance bug. Topology is a separate question: once one machine cannot hold the working set, I need a way to find a key without asking every node."

Checklist

Practice this interactively
Share this lessonWhatsAppLinkedInX

Related lessons