Backend Development6 min read

Caching Strategies

Implement multi-layer caching: in-memory, Redis, HTTP cache headers, cache-aside vs write-through, invalidation strategies, and cache stampede prevention.

cachingredisperformancebackend

Caching dramatically reduces latency and database load. Effective caching requires choosing the right cache level (CDN, HTTP, application, database), appropriate TTLs, correct invalidation strategies, and awareness of failure modes like stampede and thundering herd.

Cache at the right level: HTTP headers for public content (CDN cacheable), Redis for shared application state, in-memory for per-process frequently read data. Set TTLs conservatively - stale data is a bug. Plan invalidation before you plan caching.

Workflow

  1. Implement cache-aside with RedisCache-aside (lazy loading) is the most flexible pattern - only caches what's actually requested.
  2. Set HTTP cache headers correctlyHTTP caching at the CDN/browser layer is the most impactful for public content - zero server cost.
  3. Choose the right invalidation strategyCache invalidation is hard - wrong strategy leads to stale data or excessive cache misses.
  4. Prevent cache stampedeWhen a popular cache key expires, many requests simultaneously try to repopulate it - overloading the database.
  5. Layer caches appropriatelyMultiple cache layers (CDN → in-memory → Redis → DB) maximize performance at each stage.

Output Format

Redis cache-aside with TTL and delete-on-write, HTTP `Cache-Control` headers for public content, distributed lock for stampede prevention, `lru-cache` for in-memory layer, stale-while-revalidate for CDN.

Examples

Cache with mutex: `async function getCachedUser(id: string) { const cached = await redis.get(`user:${id}`); if (cached) return JSON.parse(cached); const lockKey = `lock:user:${id}`; const locked = await redis.set(lockKey, "1", "EX", 5, "NX"); if (!locked) { await sleep(100); return getCachedUser(id) } try { const user = await db.users.findById(id); await redis.setex(`user:${id}`, 3600, JSON.stringify(user)); return user } finally { await redis.del(lockKey) } }`
HTTP cache: `res.set({ "Cache-Control": "public, max-age=300, stale-while-revalidate=60", "ETag": `"${hashContent(data)}"`, "Vary": "Accept-Encoding" })`
LRU in-memory: `import { LRUCache } from "lru-cache"; const configCache = new LRUCache<string, Config>({ max: 100, ttl: 1000 * 60 * 5 }); // 5 min TTL`

Edge Cases & Gotchas

  • Cache poisoning: never cache responses that include user-controlled input without sanitization. Ensure `Vary` header includes all relevant dimensions (Accept-Language, cookie)
  • Redis eviction: when Redis reaches maxmemory limit, it evicts keys based on the configured policy (allkeys-lru is common). Design TTLs so important cache keys are less likely to be evicted
  • Cold start: after a cache flush or deployment, all traffic hits the database simultaneously. Use a gradual rollout or pre-warm the cache before sending full traffic to new instances

When to Break These Rules

  • Don't cache highly personalized data (per-user real-time data) at the shared Redis layer - store in the user's session. Don't cache mutable financial data (account balances) - always read fresh.

Reference Files

  • Cache-aside pattern: https://docs.microsoft.com/en-us/azure/architecture/patterns/cache-aside
  • HTTP caching: https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
  • Redis caching patterns: https://redis.io/docs/manual/patterns/
  • lru-cache npm: https://github.com/isaacs/node-lru-cache

More skills in this library

Browse all technical skills - Mobile, AI/ML, Web, Backend, Design, and Product.