InterviewKata
System design · Module 09 of 29 · Level 2: Building blocks

Caching: fast answers, and the price of keeping them fresh

A cache keeps a copy of data somewhere faster to reach than its source. Done well, it turns a 20 ms database query into a sub-millisecond lookup and lets one database serve ten times the traffic. Done carelessly, it serves stale data, falls over all at once, or melts on one popular key. This module covers where to cache, the standard patterns, eviction, invalidation and the failure modes interviewers love to ask about.

New to this?Start with B5 · Data structures behind systems in Level 0; this module assumes it.

1. The idea

You keep the spices you use daily on the counter, the rest in the cupboard, and bulk sacks in the garage. The counter is small, so when it's full something goes back to the cupboard (eviction). If you refill a jar in the garage, the counter one is now out of date (invalidation). And if all ten cooks reach for the counter jar the moment it runs out, they all walk to the garage at once (a stampede).

Caching works because access is skewed: a small fraction of the data gets most of the requests (today's news, a celebrity's profile, the home page). Keep that fraction in memory and most requests never reach the slow source.

Where caches live

LayerWhat it isGood for
ClientBrowser HTTP cache, app storageStatic files, rarely changing data; zero network. Little control once sent.
CDNEdge servers near users (Module 10)Images, video, scripts, public pages and API responses shared by many users
In-processA map inside each app serverTiny, very hot data: config, feature flags, the hottest keys. Fastest, but each server has its own copy.
Distributed cacheRedis, Valkey, Memcached, shared by all serversThe default application cache: sessions, query results, computed objects
DatabaseThe database's own buffer cache, materialised viewsHappens anyway; size memory so the working set fits

2. How it works

Cache-aside, and its classic race

Cache-aside (also called lazy loading) is the default: the application checks the cache, and on a miss reads the database and fills the cache. On a write, it updates the database and deletes the cached key. Press Play, including the race at the end.

The other patterns

Cache-aside isn't the only way to wire a cache to a database. The four alternatives differ in two questions: who loads the cache on a miss (the application or the cache itself), and when a write reaches the database (before the caller gets an answer, or later). Pick a pattern below; each numbered arrow matches a numbered line, and pointing at a line lights up its arrow.

PatternHowUse whenWatch out
Cache-asideApp reads cache → on miss reads DB, fills cache; on write updates DB, deletes keyAlmost always; the cache holds only what's readFirst read after a change is slow; delete-vs-fill races
Read-throughThe cache itself loads from the DB on a missCDNs; caching libraries with a loader functionNeeds a cache that knows your data source
Write-throughEvery write goes to the cache and the DB togetherReads must see writes immediatelySlower writes; caches data that may never be read
Write-behindWrite to the cache; flush to the DB later in batchesVery high write rates where losing a little is acceptable (counters, analytics)A cache crash loses unflushed writes
Write-aroundWrites skip the cache; reads fill itData written once and rarely read soon (logs, uploads)Recent writes always miss

Eviction

When caches go wrong

ProblemWhat happensFixes
Stampede (thundering herd)A hot key expires and thousands of requests miss at once and all hit the databaseRequest coalescing (one request rebuilds, the rest wait for it); refresh hot keys early, before expiry, or probabilistically; serve the stale value while one request refreshes
Hot keyOne key (a viral post) gets so much traffic it overloads the single cache node that holds itAn in-process cache of that key on each app server; copies under several keys (post:42#1…#8) read at random
Stale dataThe source changed, the cache didn'tDelete on write; short TTL as a safety net; versioned keys (user:7:v12) bumped in the same transaction
Cold start / cache downAn empty or failed cache sends 100% of reads to a database sized for 5%Warm the cache before taking traffic; a circuit breaker and rate limit in front of the DB; replicas of the cache
PenetrationRequests for keys that don't exist miss every timeCache "not found" briefly; a Bloom filter of existing keys

Cache invalidation: keeping the copy honest

A cache holds a copy, and a copy goes out of date the moment the original changes. Invalidation is how you decide that a cached entry can no longer be trusted. There are three ways to do it, and real systems usually combine them.

ApproachHow it worksHow stale can a read be?Cost
Expire on a timer (TTL)Every entry carries a time-to-live; after it, the next read misses and reloadsUp to the whole TTLNone to build; you trade freshness against hit ratio when picking the TTL
Delete on writeThe code that writes the database also deletes the cached keyUsually milliseconds; longer if the delete fails or races a slow readerEvery write path must remember to do it, including scripts and admin tools
Invalidate from the change logA separate worker reads the database's change stream and deletes the affected keysThe lag of the change stream, typically under a secondOne more moving part to run; but no write path can forget

The third approach is the most robust, because it watches the database itself rather than trusting every piece of code that writes to it. Here it is for a bookshop changing a book's price:

Whatever you choose, keep a TTL on every key as a safety net: if a delete is lost, the stale entry still dies on its own. And prefer deleting a key to overwriting it on a write; two writers overwriting in the wrong order leave the older value cached, while two deletes can't conflict.

Many caches, one truth

Large services often keep a small cache inside each application server (a few seconds of the hottest keys, read in microseconds) in front of the shared cache. That creates a new problem: when one server changes a value, the copies held by all the other servers are now wrong, and the database doesn't know they exist. Keeping these copies in agreement is called cache coherence.

3. Try it yourself

How big must the cache be?

Popularity in real systems often follows a Zipf curve: the 2nd most popular item gets half the traffic of the 1st, the 3rd a third, and so on. See how much traffic a small cache absorbs, and what's left for the database.

1M
1%
1.00
50,000/s

LRU vs LFU, request by request

A cache with 3 slots. Type a sequence of keys (or use a preset) and compare the policies. The "scan" preset shows a one-off sweep through many keys flushing LRU's popular items.

4. Trade-offs

Speed vs freshness

Longer TTLs mean more hits and staler data. Set the TTL from the requirement: "prices may be 30 seconds old" means a 30-second TTL, plus active invalidation for anything that must be exact.

A new thing that can fail

The cache needs capacity planning, monitoring and a plan for when it's empty or down. The database must survive a cold cache, or you need to protect it.

Don't cache what must be exact

Live seat availability, account balances and inventory at checkout should be read from the source of truth. Cache the event page, not "is seat 14C free?".

Sometimes the index is the cache

An indexed lookup on a well-sized database takes a millisecond or two. Add a cache for expensive queries and real read pressure, not by reflex.

5. In real systems

The technologies up close

The default answer in an interviewName the bottleneck with rough numbers first (profile reads are 100× writes and hit the same few profiles), then in five steps:
  1. Bottleneck: read-heavy load, an expensive query, a tight latency target.
  2. What to cache: read often, changes rarely, costly to compute; name the keys (user:123:profile).
  3. Pattern: Redis, cache-aside, plus the CDN for media and in-process for very hot keys.
  4. Eviction and freshness: LRU with a TTL set from the staleness requirement, delete on write.
  5. One or two failure modes that fit this system: stampede on hot keys, staleness for correctness, the cache going down.

Redis (and Valkey)

The default cache · know it well

An in-memory data-structure server: commands run one at a time on a single thread (so no locks), about 100,000+ operations a second per node at sub-millisecond latency. The most versatile tool to know well, and not a system of record.

How it works

  • Data structures: strings, hashes, lists, sets, sorted sets, streams, geospatial indexes, plus Bloom filters, JSON and time series in Redis 8.
  • Durability gaps: RDB snapshots lose everything since the last snapshot; the append-only file (AOF) fsyncs every second by default, so up to about a second can be lost. Replication is asynchronous, so a failover can lose acknowledged writes. Need a durable Redis? AWS MemoryDB.
  • Cluster: keys hash to 16,384 slots spread over nodes. Clients cache the slot map; a node that doesn't own a key answers MOVED. There's no query router, so a multi-key command must touch keys in one slot; hash tags ({user:123}:posts, {user:123}:likes) force related keys together.
  • Batching: pipelining and MGET make many small lookups cheap.
UseHowThe catch
Cacheproduct:123 → JSON or a hash, TTL per keyTTL handles freshness, not memory: set maxmemory and an eviction policy, or writes fail when full
LeaderboardSorted set: ZADD a score; ZREVRANGE for the top NTrim unbounded sets (ZREMRANGEBYRANK)
Rate limitingINCR + EXPIRE in one Lua script (fixed window), or a sorted set of timestamps (sliding window)Setting EXPIRE on every call means the window never resets
Nearby searchGEOADD, GEOSEARCH … BYRADIUS (Module 12)Post-filter by exact distance
Work queueStreams: XADD, consumer groups, acks; reclaim a dead worker's itemsItems can run twice: make processing idempotent. Long retention and replay → Kafka
Pub/subPUBLISH/SUBSCRIBE; sharded pub/sub in ClusterAt-most-once: offline subscribers miss messages (Module 19)
Distributed lockSET lock:seat:14C <token> NX EX 30; release only if the token matchesAn efficiency tool, not a correctness guarantee: failover or a paused holder can let two clients in (Module 21)

Status today (checked October 2026)

Redis 8 is available under three licences, including the open-source AGPLv3; Valkey, the Linux Foundation's BSD-licensed fork, is client-compatible and reached version 9 in September 2026. Managed: ElastiCache, Memorystore, Azure Managed Redis, plus MemoryDB for durability.

Use it when

  • Caching hot reads in front of a database
  • Counters, rate limits, leaderboards, sessions, geo lookups
  • Light queues and fan-out when Redis is already there

In the interview

Redis, cache-aside, LRU with a 10-minute TTL, delete on write. For a leaderboard: a Redis sorted set per board, ZADD on each score, top 10 with ZREVRANGE.

🚩 Red flags
  • Redis as the database of record for payments or orders.
  • A Redis lock guarding correctness (no double-booking) without fencing tokens or a database check. Use a row lock or conditional write for correctness.
  • Multi-key operations across a cluster without hash tags: they fail with a CROSSSLOT error.
  • No eviction policy: TTLs alone don't stop memory filling up.

Memcached

Situational · plain, huge caches

A simple, multi-threaded key-value cache: strings only, no persistence, no replication, very fast.

How it works

Clients spread keys over servers with consistent hashing; a lost server just means misses. Facebook's "Scaling Memcache" paper describes running it at billions of requests a second, with leases against stampedes and invalidations from the database's change log.

Status today

Mature and still used at very large scale; managed in ElastiCache and Memorystore. Redis has largely replaced it for new designs because of its data structures.

Use it when

  • Very large, simple key → blob caches where multi-threaded throughput per node matters

In the interview

Fine to say Redis or Memcached; pick Redis if you need sorted sets, counters or streams.

🚩 Red flags
  • Choosing Memcached, then needing leaderboards or atomic counters it doesn't provide.

In-process (local) caches

Situational · an add-on for the hottest keys

A small cache in each app server's own memory (Caffeine, Guava, an LRU map). The fastest possible read, but not shared.

How it works

Each server keeps its own copy with a short TTL. A hot key served locally never reaches Redis, so one celebrity profile can't overload one cache node. The price: each server may hold a slightly different version for a few seconds.

Status today

Common in every language; often a two-level setup: in-process (seconds) in front of Redis (minutes).

Use it when

  • Small, hot, rarely changing data: configuration, feature flags
  • Hot keys that would overload one Redis node
  • A fallback when the shared cache is down

In the interview

For the few hottest keys, a 5-second in-process cache on each server, so they never hit a single Redis node.

🚩 Red flags
  • In-process caches as the main cache: every server warms separately and invalidation becomes per-server.

Browser and CDN caching

The default for static and shared content

Responses reused by the user's browser or by CDN edge servers near users, controlled with HTTP headers.

How it works

Cache-Control says who may store a response and for how long; ETag lets clients revalidate cheaply (304 Not Modified). A CDN turns a 250 ms cross-world fetch into roughly 20–40 ms, and absorbs most traffic for popular objects (Module 10).

Status today

Universal: Cloudflare, Fastly, Akamai, CloudFront and the clouds' own CDNs.

Use it when

  • Images, video, scripts
  • Public responses many users share (catalogue pages, public posts)

In the interview

Media and public pages are served through the CDN with long TTLs; per-user data goes through Redis.

🚩 Red flags
  • Caching personal data at the CDN under a shared key: users see each other's data.

Staleness as a budget, per kind of data

The default · every cached value

"Is a stale cache OK?" has no single answer. Each kind of data has its own budget: how old a value may be before showing it is wrong. Write the budget down as a number, and the TTL, the invalidation method and where you cache all follow from it.

How it works

Data (an online bookshop)Stale budgetWhat a stale value costsSo we…
Book descriptions, cover imagesHoursA typo fix shows lateCache at the CDN with long TTLs; versioned image URLs
Bestseller list10 minutesA slightly old ranking; nobody can tellRecompute on a timer; serve from cache only
Price on the product pageAbout a minuteA shopper sees £9 and pays £7 (or the reverse: a complaint)Short TTL plus delete-on-change; checkout re-reads the price from the database
Stock count shownSeconds, shown as "few left""In stock" on an item that just sold outCache a rounded band, not the number; the order step checks the real count
Your own basket and ordersZero for you, after you change it"I added it and it vanished": a lost sale and a support ticketRead your own recent writes from the primary, or from the client's local state
Account balance, permissionsZeroMoney or access granted wronglyDon't cache, or cache only with a version that the write checks

Three patterns keep staleness safe:

  • Cache for display, check at the decision. Pages may show a cached price or stock level; the step that takes money or commits stock reads the source of truth.
  • Cache where the requirement changes. A whole product page tolerates minutes; the price inside it doesn't. Cache the page shell long and fetch the price separately, rather than giving the whole page the strictest budget.
  • Make the age visible and bounded. Every cached entry has a TTL even when it is also invalidated on change, so a lost invalidation heals within the budget. Where it helps users, show it: "prices as of 10:42".

Status today

This is how large shops and booking sites work in practice: browsing paths tolerate seconds or minutes, and payment, stock reservation and permissions always go to the source of truth.

Use it when

  • Choosing TTLs
  • Deciding which data the CDN may hold
  • Answering "what if the cache is stale?"

In the interview

Prices can be a minute stale on the product page, so a 60-second TTL plus delete-on-change; but checkout always reads the price from the database, so a stale cache never charges the wrong amount.

🚩 Red flags
  • One TTL for everything.
  • Charging, reserving or authorising from a cached value.
  • Invalidation with no TTL behind it, so a single lost delete is stale forever.

6. Remember it

Memory hook: "Check the counter, then the cupboard; clear the counter when you restock"Read from the cache first and fall back to the source; on every write, delete the cached copy rather than trying to update it.
How this hook works: what the counter and cupboard mean

What the slogan means, phrase by phrase

"Check the counter"
Look in the cache first: fast, small, holds the popular items.
"then the cupboard"
On a miss, read the source of truth and put a copy on the counter (cache-aside).
"clear the counter when you restock"
On a write, update the database, then delete the cache key. The next read refills it with the fresh value. Deleting is safer than writing the new value into the cache, which can race with other writers.

Why this picture fits: a kitchen counter has the same constraints as a cache: limited space (eviction), copies of things kept elsewhere (the source of truth), and the risk of using an old jar after the cupboard was restocked (staleness).

The story → the concept

In the kitchenIn the system
The counterThe cache (Redis, in-process)
The cupboardThe database, the source of truth
Putting a jar back to make roomEviction (LRU, LFU)
A best-before date on the jarTTL
Removing the old jar after restockingInvalidation on write
Ten cooks rushing to the garage at onceA cache stampede

Watch it on an example

  1. A product page reads product:42 from Redis: hit, 0.3 ms.
  2. The price changes: the service updates Postgres, then DEL product:42.
  3. The next read misses, reads the new price from Postgres, and stores it with a 10-minute TTL.
  4. If the delete was lost (a crash between the two steps), the TTL caps how long the old price can be shown: the safety net.

The trap it saves you from

Updating cache and database as two independent writes. If they interleave with another writer, the cache can end up holding the older value forever.

It still works when…

The "counter" is a CDN: check the edge, fall back to the origin, and purge (or version the URL) when you publish a new file (Module 10).

7. Common mistakes

Caching data that must be exactServing a cached balance or seat count leads to overselling and angry users. Read critical values from the source of truth.
No TTL and no eviction policyWithout TTLs, a missed invalidation is stale forever; without an eviction policy, a full cache starts rejecting writes.
Ignoring the cold or failed cacheAsk "what does the database see if the cache is empty?". If the answer is "10× its capacity", add protection.
Same TTL for millions of keys loaded at onceThey all expire together and cause a stampede. Add random jitter to TTLs.
Caching personalised responses at a shared layerA CDN or shared cache keyed only by URL can serve one user's page to another. Include the user in the key, or don't cache it there.

8. Design exercise (25 minutes)

  1. Pick what to cache. For a social app: user profiles, the home feed, like counts, "is this user following that user?", direct messages. For each: cache or not, where, TTL, and how it's invalidated.
  2. Size it. 10M users, 1 KB per profile, 20% of profiles get 90% of reads. How much memory does the cache need? One node or several?
  3. Break it. A celebrity's profile gets 200,000 reads a second. Which problem is this, and which two fixes would you combine?
  4. Try Redis locally (optional). docker run -p 6379:6379 valkey/valkey, then in valkey-cli: SET p:1 hello EX 10, TTL p:1, GET p:1 after 10 seconds.
  5. Write it down: the cache-aside read and write steps, and three failure modes with one fix each.

9. Self-check quiz

In cache-aside, what should a write do to the cache?

Deleting after the database write is the standard: the next read loads the fresh value. The TTL is only a safety net.

A popular key expires and the database suddenly gets thousands of identical queries. What is this?

Also called a thundering herd. Fix with request coalescing, early refresh, or serving stale while one request reloads.

A nightly report reads every product once. Which policy best keeps the truly popular products cached?

LRU treats each touched-once product as "recent" and evicts the popular ones. LFU keeps items that are used often.

Which should not be served from a cache?

Checkout must see the truth, or seats get double-sold. Cache the slow-changing parts around it.

In Redis Cluster, MGET user:1 user:2 fails. Why, and how do you make related keys work together?

Multi-key commands must stay within one slot. Hash tags hash only the braced part, so related keys share a slot.

Is a Redis SET NX lock enough to guarantee a seat is never double-booked?

Failover or a paused holder can let two clients hold the "lock". A conditional write or row lock on the seat is the real guarantee.

A product page shows a cached price (60-second TTL). How do you make sure nobody is charged a stale price?

Cache for display, check at the decision: the step that takes money reads the source of truth.

10. Go deeper

ResourceTimeWhy
Scaling Memcache at Facebook (NSDI 2013)1 hStampedes, stale sets, leases and cross-region invalidation at enormous scale; still the classic.
Caching challenges and strategies (Amazon Builders' Library)25 minHow Amazon decides when to cache, and protects services from cache failures.
Key eviction (Redis docs)15 minThe maxmemory policies and how Redis approximates LRU and LFU.
Caching (Hello Interview)20 minHow to bring caching into an interview: what to say, and which problems to raise.

11. Under the hood (optional)

How caches really decide what to keep, expire and refill, and where they go wrong. Not needed for most interviews; open it when you want to know how the products really do it. All the deep dives are listed on the Under the hood page.

Caching internalsHit-ratio maths, eviction policies with a simulator, expiry, stampedes, the stale-fill race, hot keys, HTTP caching, 6 products comparedabout 35 min

A cache is a bet that the recent past predicts the near future. How it decides what to keep, when entries expire, and what happens in the moment between a miss and a refill decides whether it helps or quietly serves wrong data.

Hit-ratio arithmetic

The hit ratio works on misses, and misses are what reach the database. Going from 90% to 99% looks small but cuts database load tenfold.

Where the cache sits in reads and writes

PatternReadWriteWatch out for
Cache-aside (lazy loading)App checks the cache; on a miss reads the database and fills the cacheApp writes the database, then deletes the cache entryThe fill race below; the default choice
Read-throughThe cache library or service loads misses itselfAs cache-asideSame as cache-aside, logic in one place
Write-throughFrom the cacheWrite goes to the cache and the database togetherEvery write pays both; cache fills with data nobody reads
Write-behind (write-back)From the cacheWrite to the cache; flush to the database later in batchesData loss if the cache dies before flushing
Refresh-aheadPopular entries are reloaded before they expireUnchangedWasted reloads for entries that went cold

Eviction: what to throw away

When the cache is full, the eviction policy picks a victim. Real workloads are skewed (a few keys get most requests) and occasionally interrupted by scans (a batch job or crawler touching thousands of keys once). Run each policy on the same trace:

PolicyHowWhere
LRUEvict the least recently used: a hash map plus a linked listThe textbook default; Memcached uses a segmented LRU (hot, warm, cold)
Sampled (approximated) LRU / LFUPick a few random keys (Redis samples 5 by default) and evict the best candidate among them: no list to maintainRedis allkeys-lru, allkeys-lfu and the volatile-* policies
LFUEvict the least frequently used; needs decay or old favourites stay foreverRedis LFU keeps an 8-bit logarithmic counter per key that decays over time
CLOCK, 2Q, ARCCheap LRU approximations (CLOCK) or two lists separating once-seen from repeatedly-seen keys (2Q, ARC) to resist scansOperating-system page caches, database buffer pools
W-TinyLFUA small LRU window admits newcomers; to enter the main cache a candidate must be more frequent than the victim, judged by a compact count-min sketch that halves periodicallyCaffeine (Java), Ristretto (Go)
S3-FIFOSimple FIFO queues: a small one for new keys, a main one, and a ghost list; most one-hit wonders never reach the main queueNewer caches (2023 research)

Expiry

  • Lazy expiry: an expired key is deleted when someone reads it. Cheap, but keys nobody reads would linger forever.
  • Active expiry: Redis also runs a background cycle that samples keys with a TTL many times a second and deletes expired ones, repeating while a large share of the sample is expired.
  • TTL jitter: 10,000 entries filled at the same moment with the same TTL all expire together. Add a random ±10% so refills spread out.
  • TTL is for freshness, not memory: without a maxmemory and an eviction policy, a full Redis rejects writes (the default policy is noeviction).

Stampedes and request coalescing

  • Single-flight (request coalescing): only the first miss for a key goes to the database; others wait for its result. Inside one process this is a map of in-flight futures; across servers, a short lock in the cache (SET lock:k NX EX 5).
  • Serve stale while refreshing: keep the old value past its TTL for a grace period and refresh in the background (HTTP stale-while-revalidate, Varnish grace).
  • Refresh early, probabilistically: each reader refreshes with a probability that rises as expiry approaches, so one reader refreshes before everyone misses (the "XFetch" algorithm).
  • Negative caching: cache "not found" briefly too, or repeated lookups of missing keys (often attacks) go straight to the database.

The stale-fill race

Cache-aside with "delete on write" still has a gap: a slow reader can put an old value back after the writer deleted it.

Fixes: leases (the cache hands the first misser a token; a delete invalidates it, so the late fill is refused, as Facebook's memcache does); versioned values (only set if newer); invalidate from the database's change stream (change data capture), with a second, delayed delete as a cheap safety net; and always a TTL so any stale entry eventually dies.

Hot keys, two tiers and memory

  • Hot keys: one key on one cache node can exceed that node's network or CPU. Replicate it under several names (k#1…k#8) and read a random copy, or put a tiny in-process cache (seconds) in front.
  • Two tiers: in-process cache (microseconds, per server) in front of Redis (sub-millisecond, shared). Invalidate both, for example by publishing invalidations on a channel.
  • Memory overheads: every Redis key costs tens of bytes beyond its value, so millions of tiny keys add up; small hashes, lists and sets use compact encodings (listpacks, intsets) until they pass a size threshold. Memcached stores items in fixed slab classes, so a change in value sizes can leave memory stranded in the wrong class (slab rebalancing moves pages over).
  • Snapshots fork: a Redis RDB snapshot forks the process; pages written during the snapshot are copied, so leave headroom (up to double memory under heavy writes).

HTTP caching semantics

HeaderMeaning
Cache-Control: max-age=60Fresh for 60 seconds in any cache
s-maxage=600Overrides max-age for shared caches (CDNs, proxies)
private / publicOnly the browser may store it / shared caches may too
no-cache vs no-storeStore it but revalidate before every use / never store it at all
immutableIt will never change: don't even revalidate (for fingerprinted file names)
stale-while-revalidate=30, stale-if-error=86400Serve stale while refreshing in the background / when the origin is failing
ETag + If-None-MatchRevalidation: the server answers 304 Not Modified with no body if unchanged
Vary: Accept-EncodingCache separate copies per value of that request header (too many values fragment the cache)
Surrogate-Key / cache tagsCDN-specific tags so one purge clears every page showing a product

Product by product

ProductMemory and threadingEviction and persistenceThe detail worth knowing
Redis / ValkeyOne thread runs commands (I/O threads help with sockets); compact encodings per data typeSampled LRU or LFU; lazy plus active expiry; RDB snapshots (fork) and an append-only fileOne slow command (KEYS, a huge range) blocks everyone
MemcachedMulti-threaded; slab allocator with fixed size classesSegmented LRU per slab class; no persistence; clients shard with consistent hashingSlab classes can starve when value sizes shift
Caffeine, RistrettoInside the application processW-TinyLFU / TinyLFU admissionFastest; every instance has its own copy to invalidate
Varnish, NGINX proxy cacheHTTP reverse-proxy cachesTTL from headers; grace serves stale while refreshingCoalesce identical misses into one origin request
CDNsEdge locations plus a tiered or shield layerPurge by URL, tag or everything; cold content evictedA shield tier cuts origin load; tags make purges precise
BrowsersMemory and disk cache per originCache-Control, ETag revalidation, service-worker cachesFingerprinted file names can be cached for a year

Say it in the interview

Answers that show depth
  • "What happens when a hot key expires?" A stampede: thousands of misses hit the database at once. Coalesce them (single-flight or a short lock), serve stale while one request refreshes, and add TTL jitter.
  • "Can cache-aside serve stale data forever?" Until the TTL, yes: a slow reader can refill an old value after the writer's delete. Leases or versioned sets close the race; a TTL bounds it.
  • "Why not plain LRU?" A single scan flushes the hot set. Frequency-aware admission (W-TinyLFU) keeps the hot set and refuses one-hit wonders.

The hit ratio rises from 90% to 99%. What happens to the load on the database?

Misses fall from 10% to 1% of requests.

What does Cache-Control: no-cache mean?

no-store is the one that forbids storing.

How does Redis implement LRU eviction?

Sampling avoids per-key list pointers; more samples get closer to true LRU.

Sources