karthik.dev
Back to blogBackend Engineering

Redis Caching Patterns That Actually Work in Production

2026-06-15 · 7 min read

Redis is one of those technologies where the tutorial makes it look trivial and production reveals its depth. After integrating it into CodeMyFYP Academy — a multi-tenant EdTech platform with five user roles — here's what I actually learned.

The cache-aside pattern (and why it's the safe default)

Cache-aside is the most common pattern for a reason: it's simple, it degrades gracefully, and it gives you explicit control over what gets cached.

1. Read from cache
2. On cache miss → read from DB → write to cache → return
3. On write → update DB → invalidate cache key

The advantage: if Redis goes down, the application falls back to the database automatically. The disadvantage: the first request after a cache miss is slow, and under high concurrency, multiple requests can all miss simultaneously and stampede the database.

Solving the thundering herd with a soft lock

For hot data (course listings, landing pages), a thundering herd can overwhelm your database the moment a key expires. The fix is a "soft lock" — the first request to miss the cache sets a temporary lock key and regenerates, while subsequent concurrent requests wait briefly and retry:

const lock = await redis.set(`lock:${key}`, "1", "NX", "EX", 5);
if (!lock) {
  // Wait briefly then retry from cache
  await sleep(100);
  return redis.get(key);
}
// We have the lock — regenerate
const data = await db.query(...);
await redis.setex(key, TTL, JSON.stringify(data));
await redis.del(`lock:${key}`);
return data;

TTL strategy for different data types

Not all data has the same staleness tolerance. On CodeMyFYP Academy I used three tiers:

Data type TTL Rationale
Course listings 10 min Changes infrequently; stale for a few minutes is fine
User session 24 hr Active sessions need persistence; refreshed on activity
Dashboard stats 30 sec Near-real-time feel without hammering the DB on every page load
Search results 5 min Expensive to compute; low tolerance for extreme staleness

Cache invalidation: the hard part

The famous quote — "there are only two hard things in computer science: cache invalidation and naming things" — is famous because it's true.

My rule: invalidate by event, not by TTL alone. When an instructor publishes a new course, don't wait for the 10-minute TTL to expire. Fire an explicit DEL courses:listing when the publish event happens. This keeps the cache consistent without compromising the TTL safety net.

For multi-tenant data (each university's student list), I use namespaced keys: university:${id}:students. On university-level events, I can invalidate the entire namespace with SCAN + DEL rather than tracking individual keys.

What I'd do differently

I'd instrument cache hit/miss rates from day one. Without metrics, you're flying blind — you don't know if your TTLs are too aggressive (low hit rates, DB hammering) or too conservative (stale data complaints). A simple counter per key pattern fed into a dashboard is worth an afternoon of setup.