arrow_backBACK TO REDIS — THE IN-MEMORY DATA-STRUCTURE STORE, END TO END
Lesson 04Redis — The In-Memory Data-Structure Store, End to End5 min read

Caching Patterns

October 30, 2026

TL;DR

Caching is Redis's most common job. CACHE-ASIDE (lazy loading) is the default: the app checks Redis; on a miss it loads from the database and populates the cache with a TTL. It's simple and resilient (cache down ≠ app down). WRITE-THROUGH writes to cache and DB together (fresh, slower writes); WRITE-BEHIND writes to cache and flushes to the DB async (fast, riskier). The three bugs that bite everyone: (1) STALE DATA / invalidation — cache and DB drift apart; fix with a TTL (bounded staleness) and/or invalidate on write; cache invalidation is famously hard. (2) CACHE STAMPEDE / thundering herd — a hot key expires and thousands of requests all miss and hit the DB at once, flattening it; fix with a short lock so one request refills while others wait, or probabilistic early refresh. (3) CACHING EVERYTHING with no TTL — memory fills and you cache data that's never re-read; cache the hot, re-read data with a TTL.

Caching is why most teams adopt Redis: put a fast in-memory layer in front of a slow database or API, and reads that took tens of milliseconds now take a fraction of one. The patterns are simple, but caching has a well-earned reputation for subtle bugs — stale data, thundering herds, memory blowups. This lesson is the patterns and the three traps, so you get the speed without the incidents.

The patterns

Caching patterns. Cache-aside (lazy loading), the default pattern: app does GET to Redis; on a miss, query the database, then SET the cache; the next read is a cache hit. The patterns and the three classic cache bugs: cache-aside — app checks cache, on a miss loads from DB and populates with a TTL, simple and resilient (cache down is not app down), the default; write-through — write to cache and DB together (fresh, slower writes); write-behind — write to cache, flush to DB async (fast, riskier). The three bugs: stale data / invalidation — cache and DB drift apart, use a TTL (bounded staleness) and/or invalidate on write; cache stampede / thundering herd — a hot key expires and thousands of requests all miss and hit the DB at once, fix with a short lock so one request refills while others wait, or probabilistic early refresh; caching everything with no TTL — memory fills and you cache things never re-read, cache the hot re-read data with a TTL.

Cache-aside (lazy loading) is the default and by far the most common. The application manages the cache:

  1. On a read, the app checks Redis first (GET).
  2. Hit → return the cached value (fast path, microseconds).
  3. Miss → load from the database, populate the cache with a TTL (SET ... EX), and return the value.

So the cache fills on demand with the data that’s actually requested, and subsequent reads hit the cache. Its great virtue is resilience: the cache is an optimisation, not a dependency — if Redis is down, the app falls back to the database (slower, but working). This is the pattern to use unless you have a specific reason not to.

The alternatives change when the cache is written:

  • Write-through — on a write, update the cache and the database together (synchronously). The cache is always fresh, but writes are slower (two systems) and you cache data that may never be read.
  • Write-behind (write-back) — on a write, update the cache and flush to the database asynchronously later. Fastest writes, but you risk losing buffered writes if Redis dies before flushing — riskier, used where write latency is critical and some loss is tolerable.

For most systems, cache-aside with a TTL is the right default. The others are specialisations for specific freshness/latency needs.

Bug 1: stale data and invalidation

The fundamental tension of caching: the cache holds a copy, and the moment the source data changes, the copy is stale. How do you keep them consistent?

  • TTL (bounded staleness) — give every cache entry an expiry. The data can be stale for at most the TTL, then it’s reloaded fresh. Simple, robust, and usually good enough: a 60-second TTL means data is never more than a minute old, which most features can tolerate. This is the first and best tool.
  • Invalidate on write — when the source data changes, delete (or update) the cache entry so the next read reloads it. More precise (no staleness window) but harder to get right: you must find and invalidate every cache entry affected by a write, and miss one and it stays stale.

“There are only two hard things in computer science: cache invalidation and naming things” is a joke precisely because invalidation genuinely is hard. The pragmatic approach: use a TTL by default (bounded staleness with no invalidation logic), and add explicit invalidation only where staleness is unacceptable (e.g. a price change must be immediate). Don’t try to make the cache perfectly consistent everywhere — decide per data how fresh it must be, and use the cheapest mechanism that meets it.

Bug 2: the cache stampede

Here’s a bug that stays hidden until traffic is high, then takes down your database. A hot key — one requested thousands of times a second — expires. Suddenly every one of those thousands of concurrent requests finds a cache miss at the same instant, and they all fall through to the database simultaneously. The database, which was comfortably serving one query’s worth of that data, is now hit with thousands of identical queries at once — a thundering herd that can flatten it. (And often the first few requests haven’t repopulated the cache before the next thousand miss, so it cascades.)

Fixes:

  • A refill lock — on a miss, the first request acquires a short lock (Lesson 10), refills the cache, and releases; the others wait briefly for the cache to be populated rather than all hitting the database. One DB query instead of thousands.
  • Probabilistic early refresh — refresh the key slightly before it expires, with a probability that rises as expiry approaches, so one request refreshes it early while it’s still serving from cache — the herd never all misses at once.
  • Never-expiring + background refresh — keep the hot key and refresh it on a schedule in the background, so reads never miss.

The stampede is the cache bug most likely to cause an outage, so for genuinely hot keys, plan for it rather than discovering it under load.

Bug 3: caching everything

The last trap is overuse. It’s tempting to cache everything, but:

  • Caching data that’s never re-read wastes memory and effort — the whole point of a cache is to serve repeated reads. Caching a value read once gains nothing and costs RAM.
  • Caching with no TTL fills memory (Lesson 3) with entries that never leave, eventually triggering eviction or an OOM.

The discipline: cache the hot, re-read data, with a TTL. Profile what’s actually read repeatedly (and is expensive to compute) and cache that. Leave rarely-read or cheap-to-fetch data uncached. A smaller cache of genuinely hot keys outperforms a giant cache of everything, and uses a fraction of the memory.

Put together: use cache-aside with a TTL as your default, keep data fresh with TTLs (plus targeted invalidation), protect hot keys from stampedes, and cache only hot re-read data. Do that and Redis caching is the big, cheap performance win it’s famous for. Next we turn to making Redis itself faster: pipelining and transactions.

Pitfalls to Avoid

  1. No TTL on cache entries. Without expiry, the cache fills memory and data can stay stale forever. Give every cache entry a TTL (bounded staleness + memory control).
  2. Over-engineering invalidation everywhere. Trying to make the cache perfectly consistent is hard and bug-prone. Use a TTL by default; add explicit invalidation only where staleness is truly unacceptable.
  3. Ignoring cache stampedes. A hot key expiring sends a thundering herd to the database and can cause an outage. Use a refill lock, probabilistic early refresh, or background refresh for hot keys.
  4. Caching everything. Caching rarely-read or cheap data wastes memory for no gain. Cache hot, repeatedly-read, expensive-to-compute data only.
  5. Treating the cache as a dependency. If your app can’t serve a request when Redis is down, you’ve made the cache a single point of failure. Cache-aside should degrade to the source on a cache miss/outage.
  6. Caching without considering consistency needs. Different data tolerates different staleness. A product description can be minutes old; a price or inventory count may need immediate invalidation. Decide per data.

Quick Recap

  • Cache-aside (lazy loading) is the default: check cache → on miss, load from DB and populate with a TTL. Resilient (cache down ≠ app down). Write-through (fresh, slower writes) and write-behind (fast, riskier) are specialisations.
  • Bug 1 — stale data: use a TTL for bounded staleness by default; add invalidate-on-write only where needed. Invalidation is genuinely hard.
  • Bug 2 — cache stampede: a hot key expiring sends a thundering herd to the DB. Fix with a refill lock, probabilistic early refresh, or background refresh.
  • Bug 3 — caching everything: wastes memory. Cache hot, re-read, expensive data with a TTL only.

Next lesson: making Redis itself faster — pipelining and transactions (the measured 16–25× throughput lever).