How and when do you cache in a Node application?

3 minintermediatenodejscachingredisperformanceinvalidation

Quick Answer

Cache expensive or frequently-read, infrequently-changing data to cut latency and load. Options range from in-process memory (fast but per-instance and volatile) to a shared store like Redis (consistent across instances). The hard part is invalidation — use TTLs and explicit invalidation on writes.

Detailed Answer

Answer:

Why cache: avoid repeating expensive work — DB queries, external API calls, heavy computation — for data that's read often and changes rarely.

Layers of caching:

  1. In-process memory (a Map or lru-cache): fastest, zero network hop.
    • ❌ Not shared across instances; lost on restart; can bloat memory. Fine for small, hot, per-instance data.
  2. Distributed cache (Redis/Memcached): shared across all instances, survives restarts, supports TTLs and rich structures.
async function getUser(id) {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);

  const user = await db.getUser(id);
  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 300); // 5 min TTL
  return user;
}
  1. HTTP caching (Cache-Control/ETag) and a CDN for static or public responses — offload work entirely.

Invalidation — the hard part:

  • TTL/expiry — simplest; accept slightly stale data for a bounded window.
  • Write-through / explicit invalidation — delete or update the cache key when the underlying data changes.
async function updateUser(id, data) {
  const user = await db.updateUser(id, data);
  await redis.del(`user:${id}`);   // invalidate on write
  return user;
}

Watch out for:

  • Stale data — pick TTLs that match tolerance for staleness.
  • Cache stampede — many misses hitting the DB at once when a hot key expires (mitigate with locks/"single-flight" or jittered TTLs).
  • Don't cache per-user sensitive data in shared/public caches.

Rule: cache read-heavy, change-rarely data; always have an invalidation strategy (at minimum a TTL).