All Writing
Performance8 min read

How Redis caching actually works.

Why the second request is always faster than the first — traced through an actual cache miss and an actual cache hit.

Every database query costs something — disk I/O, CPU, time. If a thousand people load the same product page in the same minute, a plain app re-runs the exact same expensive query a thousand times, for an answer that didn’t change at all between request #1 and request #1000. Caching is just refusing to do that: keep the answer sitting in memory, and hand out the copy instead of recomputing it. Redis is the tool most backends reach for to do this — an in-memory key-value store built to answer in well under a millisecond.

The one-sentence version
A cache is a fast, temporary copy of a slow answer — check the copy first, and only pay the full price when the copy doesn’t exist yet.

What Redis actually is

Redis (REmote DIctionary Server) is a key-value store that lives almost entirely in RAM, which is what makes it so fast — reading from memory is roughly 50–100x faster than reading from disk-backed storage like Postgres or MySQL. Unlike a plain dictionary though, Redis understands several data shapes natively: strings, hashes, lists, sets, and sorted sets — so it isn’t just a cache, it’s a small, extremely fast database that most teams happen to use as a cache.

Every lookup is either a hit or a miss

Cache miss

  • Redis has nothing stored under that key
  • The server has to fall back to the real database
  • Slower — this request pays the full cost
  • Ends with the server writing the result into Redis

Cache hit

  • Redis already has the answer in memory
  • The database is never touched
  • Fast — often sub-millisecond
  • This is the state you want almost every request in

The first request — a cache miss

Nobody has asked for this data recently, so Redis comes up empty and the server has to go the slow way, then save its work on the way out.

Client
Server
Redis
Database
Step 1 of 7

A request comes in for data the server has never been asked for before — or hasn't been asked for in a while.

Every request after — a cache hit

Same endpoint, same data — but now Redis already has it. Watch how much shorter this round trip is, and notice the database doesn’t appear in it at all.

Client
Server
Redis
Step 1 of 4

The exact same request — this time Redis already has the answer from the last request that warmed it.

0.1–1ms

Typical Redis read

10–50ms+

Typical DB query

30s – 1hr

Common TTL range

How long should the data live?

That EX 300 in the miss diagram is a TTL — time to live, in seconds. It’s what keeps a cache from quietly turning into a database full of lies: without an expiry, the first write stays frozen in Redis forever, even after the real data changes underneath it. Short TTLs keep data fresher but cache less effectively; long TTLs cache better but risk staleness. Most teams start around 30 seconds to a few minutes for data that changes occasionally, and go much longer for data that’s basically static.

Common mistake
Caching something with no expiry at all. It feels harmless in development with a handful of keys, and then quietly becomes an unbounded memory leak in production once there are millions of unique keys that never get cleaned up. Default to setting a TTL on everything, and treat “cache forever” as a deliberate, rare exception.

Cache invalidation — the actually hard part

There’s an old joke that there are only two hard problems in computer science: cache invalidation, naming things, and off-by-one errors. The TTL above is the lazy version — wait for the clock to run out. Two other strategies handle it more deliberately:

  • Write-through invalidation — the moment the underlying data changes (an UPDATE to that product), the server explicitly runs DEL product:42 so the next read is forced to be a fresh miss.
  • Event-driven invalidation — a change anywhere in the system publishes an event (via Redis PUBLISH or a message queue), and any service holding a cached copy clears it in response.
Common mistake
Cache stampede (thundering herd): a hugely popular key expires, and a thousand simultaneous requests all get a miss at once — all thousand hit the database simultaneously, which can be worse than having no cache at all. Common fixes: add random jitter to TTLs so keys don’t all expire at the exact same moment, or have only the first request rebuild the value while the rest wait on it (“single-flight”).

What happens when Redis runs out of memory

Redis has a configurable maxmemory limit. Once it’s full, it needs an eviction policy to decide what to throw away to make room for new writes:

  • allkeys-lru — evict whatever hasn’t been read least recently, across every key. The most common default for pure caching workloads.
  • volatile-lru — same idea, but only among keys that have a TTL set, leaving permanent keys untouched.
  • allkeys-lfu — evict whatever is read least frequently overall, which handles “popular most of the time, but one huge burst” keys better than LRU does.
  • noeviction — refuse new writes once full, and return errors instead. Appropriate when Redis is holding data you can’t afford to silently lose, not just cache.

Redis is more than just a cache

The same in-memory speed makes Redis a natural fit well beyond “remember this API response”:

  • Session storage — exactly the shared session store from the sessions post: every app server reads the same Redis instance, so it doesn’t matter which server handles the next request.
  • Rate limiting — a counter per user per time window, incremented atomically on every request, checked against a limit before the request is allowed through.
  • Leaderboards — Redis’s sorted-set type keeps members ordered by score automatically, so “top 10 players right now” is a single ZRANGE call, not a sort over the whole table.
  • Pub/sub & queues — lightweight real-time messaging between services, or a simple job queue, without standing up a separate broker.
In practice
Most teams reach for Redis the moment a single endpoint needs to survive real traffic without hammering the database on every request — or the moment they need state shared across more than one server instance, which is exactly the problem the sessions post covers. The two show up together constantly in practice.