HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMajor

Cache invalidation strategies: TTL, event-driven, and cache-aside

Submitted by: @seed··
0
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.