Adding a cache takes ten minutes. Debugging the bug it causes six months later takes a week.
Caching trades correctness for speed. The senior move is deciding how much correctness you're willing to give up, and making that decision explicit.
Start with the question, not the tool
Before you reach for Redis, answer three things:
- How stale can this data be? (seconds, minutes, never)
- What does a wrong answer cost? (a flicker, or a wrong invoice)
- What's the read-to-write ratio? (caches pay off on read-heavy data)
If you can't answer #1, you're not ready to cache it.
The four patterns
CACHE-ASIDE (lazy) WRITE-THROUGH
App -> Cache: hit? return App -> Cache -> DB
miss -> DB -> fill (write both, in order)
WRITE-BEHIND READ-THROUGH
App -> Cache -> (async) DB App -> Cache (cache loads DB itself)
| Pattern | Good for | Risk |
|---|---|---|
| Cache-aside | Most apps, default choice | Stale reads, stampedes |
| Write-through | Data read right after write | Slower writes |
| Write-behind | Write-heavy, loss-tolerant | Data loss if cache dies |
| Read-through | Simplifying app code | Provider lock-in |
My default: cache-aside with a TTL. It's simple, and failure is safe because the DB stays the source of truth.
Cache-aside in code
async function getUser(id) {
const key = `user:${id}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const user = await db.users.findById(id);
if (user) {
// TTL + jitter so keys don't all expire together
const ttl = 300 + Math.floor(Math.random() * 60);
await redis.set(key, JSON.stringify(user), "EX", ttl);
}
return user;
}
async function updateUser(id, data) {
await db.users.update(id, data);
await redis.del(`user:${id}`); // invalidate, don't update
}
Delete on write, don't update. Updating the cache invites race conditions where an older write lands after a newer one.
The three failure modes
1. Cache stampede
A hot key expires and thousands of requests hit the database at once.
t=0 key expires
t=0+ [req][req][req][req][req] -> all miss -> all hit DB -> DB falls over
Fixes:
- Request coalescing (single flight): one request rebuilds, the rest wait.
- Lock on rebuild with a short timeout.
- Stale-while-revalidate: serve the old value while one worker refreshes.
- TTL jitter, as in the code above.
const inflight = new Map();
function singleFlight(key, loader) {
if (inflight.has(key)) return inflight.get(key);
const p = loader().finally(() => inflight.delete(key));
inflight.set(key, p);
return p;
}
2. Cache penetration
Requests for keys that don't exist always miss and always hit the DB (bots love this).
Fix: cache the negative result briefly (null with a 30-60s TTL), or put a Bloom filter in front.
3. Stale data after writes
Thread A: reads DB (old) ............ writes cache (old)
Thread B: updates DB (new), deletes cache
Result: cache now holds OLD data until TTL
Fixes: short TTLs as a safety net, versioned keys (user:42:v7), or event-driven invalidation from the write path.
Layers matter
Browser cache -> CDN -> API gateway -> App memory -> Redis -> DB
(fastest, least control) ------------------> (slowest, most control)
Push caching as close to the user as correctness allows. For public, static-ish data, HTTP headers do more than any Redis cluster:
Cache-Control: public, max-age=60, stale-while-revalidate=300
What to measure
- Hit ratio (below ~80% on a hot path means rethink the key design)
- p99 latency with and without cache
- Eviction rate and memory pressure
- DB load during cold start: can you survive an empty cache?
That last one is the real test. If a cache flush takes you down, your cache isn't an optimization, it's a dependency.
Rules I follow
- The database is the source of truth, always.
- Every key has a TTL. No exceptions.
- Invalidate by deleting, not by updating.
- Design for a cold cache on day one.
- Don't cache what you can't afford to be wrong.
Takeaway
There are only two hard things in computer science: cache invalidation and naming things. Treat invalidation as a design problem up front, and your cache stays a speed boost instead of becoming a source of mysterious bugs.
What's the nastiest caching bug you've shipped? Share it in the comments. 👇
Top comments (0)