SE

Caching with Redis

Must-know concept1.5 hIntermediate

Cache-aside, write-through, TTL, invalidation.

"There are only two hard things in computer science: cache invalidation and naming things." Get cache invalidation right and Redis is the cheapest 100× speedup you'll ever deploy. Get it wrong and users see stale data forever.

The big idea

A cache is a fast, small store of computed answers. Three things define a cache: when you populate it, when you invalidate it, and what happens when it's wrong. A cache without an answer to all three is a memory leak.

The standard strategies

AttributeStrategyTrade-off
Cache-asideApp reads cache first; on miss, reads DB and writes back to cache.Simple. Stale reads possible if the DB changes without invalidation.
Write-throughEvery write goes to cache + DB in lock-step.Always-fresh reads; slower writes; cache and DB can drift on partial failure.
Write-behindWrite to cache, flush to DB asynchronously.Fastest writes; risk of data loss if the cache dies before flushing.
Refresh-aheadBackground job re-populates hot keys before they expire.Hides latency for predictable hot keys; wastes work on cold ones.

Cache-aside in code

The default for most apps:

async function getUser(id: number): Promise<User | null> {
  const key = `user:${id}`;
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
 
  const user = await db.user.findUnique({ where: { id } });
  if (user) {
    await redis.set(key, JSON.stringify(user), 'EX', 300);   // 5 min TTL
  }
  return user;
}
 
async function updateUser(id: number, patch: Partial<User>) {
  await db.user.update({ where: { id }, data: patch });
  await redis.del(`user:${id}`);                              // invalidate
}

Three things to notice: TTL on the SET, explicit invalidation on writes, and serialisation (Redis stores strings).

TTLs and invalidation

Two ways out of "stale forever":

  1. Time-based (TTL)

    Every key expires after EX seconds. Pick a TTL short enough to be tolerable, long enough to be useful. 5 minutes is a sane default; 1 second is a denial-of-service on your DB.

  2. Event-based

    On every write, DEL the affected keys. The cache repopulates on next read. More consistent, but you must know every key affected by a write.

Most production systems use both: event-based invalidation for known writes plus a TTL as a safety net for the bugs you missed.

Common pitfalls

Eviction policy

Redis must drop something when it runs out of memory. Configure maxmemory-policy explicitly — the default depends on the version and surprises everyone.

allkeys-lru
Drop least-recently-used keys; safe default for caches.
volatile-lru
Same but only keys with a TTL — use if Redis also holds non-cache state.
allkeys-lfu
Least-frequently-used; better for skewed access patterns.
noeviction
Reject writes when full. Use only when Redis is your source of truth, never as a cache.

Beyond GET/SET

Redis has data types that make it more than a key-value store:

# Counter
INCR  rate:42:minute
EXPIRE rate:42:minute 60
 
# Set (for "have I seen this user today?")
SADD  daily_active:2025-11-12 42
 
# Sorted set (leaderboard)
ZADD  leaderboard 1500 alice
ZRANGE leaderboard 0 9 REV WITHSCORES
 
# Pub/sub
PUBLISH chat:room42 "hello"
SUBSCRIBE chat:room42

In practice

The biggest cache wins are at the boundary of a slow operation: a complex aggregation, a third-party API call, a per-page render. Cache the answer, not the input. And keep the cache simple — when you find yourself building a "cache that's almost a DB," you've gone too far.

Key takeaways

  • Cache-aside (read-through with explicit invalidation) is the default strategy.
  • TTLs are your safety net; event-based invalidation is your primary mechanism.
  • Beware the thundering herd — jitter, locks, or refresh-ahead.
  • Set `maxmemory-policy` explicitly; the default surprises everyone.
  • Redis is more than a key-value store — counters, sets, sorted sets, pub/sub.

Checkpoint questions

Use these to test whether the lesson is clear enough to explain without rereading.

  1. 1Which cache strategy fits a read-heavy endpoint with occasional writes?
  2. 2What could make cached data stale, and how would you invalidate it?
  3. 3How do TTLs reduce risk without fully solving correctness?
  4. 4What is a cache stampede, and how can you reduce it?

References

External resources for going deeper after the lesson above.