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.