patternjavascriptMajor
Cache invalidation strategies: TTL, event-driven, and cache-aside
Viewed 0 times
cache invalidationcache-asidewrite-throughstale-while-revalidateRedis TTLcache stampede
Problem
Cached data becomes stale after the underlying data changes, causing users to see outdated information. Or the cache is invalidated too aggressively, eliminating the performance benefit.
Solution
Choose the right strategy for each use case:
// 1. Cache-Aside (most common): check cache, miss -> load from DB, store in cache
async function getUser(userId) {
const cacheKey = `user:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
await redis.setEx(cacheKey, 3600, JSON.stringify(user)); // TTL: 1 hour
return user;
}
// 2. Event-driven invalidation: invalidate on write
async function updateUser(userId, data) {
await db.query('UPDATE users SET ... WHERE id = $1', [userId]);
await redis.del(`user:${userId}`); // Invalidate immediately
}
// 3. Write-through: update cache and DB together
async function updateUserWriteThrough(userId, data) {
await Promise.all([
db.query('UPDATE users SET ... WHERE id = $1', [userId]),
redis.setEx(`user:${userId}`, 3600, JSON.stringify({ id: userId, ...data })),
]);
}
// 4. Stale-while-revalidate: return stale, refresh in background
async function getWithSWR(key, fetchFn, { ttl = 60, staleTtl = 300 } = {}) {
const cached = await redis.get(key);
if (cached) {
const { data, cachedAt } = JSON.parse(cached);
const age = (Date.now() - cachedAt) / 1000;
if (age > ttl) {
// Refresh in background, return stale data now
fetchFn().then(fresh =>
redis.setEx(key, staleTtl, JSON.stringify({ data: fresh, cachedAt: Date.now() }))
);
}
return data;
}
const data = await fetchFn();
await redis.setEx(key, staleTtl, JSON.stringify({ data, cachedAt: Date.now() }));
return data;
}Why
Cache-Aside is the safest pattern because the application controls what goes in the cache. Event-driven invalidation minimizes staleness for frequently updated data.
Gotchas
- Cache stampede (thundering herd): when a popular key expires, many requests hit the DB simultaneously — use a lock or probabilistic early expiration
- Race condition: update DB then invalidate cache. If another request reads between update and invalidation, it will cache the old value — add a short TTL as safety net
- Never cache errors — a failed DB query should not be cached as empty result
- Namespace cache keys by version: v2:user:${id} makes cache busting easy during schema changes
Revisions (0)
No revisions yet.