Skip to content
← All posts

Backend ServicesBy 06 May 20263 min read

Caching Strategies with Redis: Patterns, Pitfalls and Invalidation

The right cache can cut response times and database load dramatically — the wrong one serves stale data. Cache-aside, write-through, TTLs, stampede protection and invalidation with Redis.

Caching Strategies with Redis: Patterns, Pitfalls and Invalidation

"There are only two hard things in computer science: cache invalidation and naming things." The quote is a joke, but the first half is true. Caching is one of the most effective ways to make a backend faster and cheaper — and one of the easiest ways to show users stale or wrong data. Redis remains the most widely used tool for the job.

The cache-aside patternRead from the cache first; on a miss, load from the database and populate the cache.

What to Cache

Good candidates are read often, change rarely, and expensive to compute:

  • configuration and reference data (plans, categories, settings),
  • results of heavy queries and aggregations (dashboard statistics, reports),
  • rendered fragments or API responses for public content,
  • sessions and rate-limit counters.

Poor candidates: data that must always be exactly current (account balances), or data that is cheap to fetch anyway.

Cache-Aside (Lazy Loading)

The most common pattern: the application checks the cache, and on a miss loads from the database and stores the result.

async function getCourse(tenantId: string, id: string) {
  const key = `tenant:${tenantId}:course:${id}`;
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const course = await db.course.findFirst({ where: { tenantId, id } });
  if (course) await redis.set(key, JSON.stringify(course), { EX: 300 }); // 5-minute TTL
  return course;
}

Note the tenant in the key. In a multi-tenant system, a cache key without the tenant is a data leak waiting to happen.

Write-Through and Write-Behind

  • Write-through updates the cache and database together on every write — reads are always warm, at the cost of slower writes.
  • Write-behind writes to the cache first and persists asynchronously — fast, but risks data loss and is rarely worth the complexity for application data.

Invalidation Strategies

  • TTL (time-to-live) — simplest and safest default. Choose a TTL that matches how stale the data can acceptably be.
  • Explicit deletion on write — when a record changes, delete its key so the next read reloads it.
  • Versioned keys — include a version or updated_at in the key; old entries expire naturally.

Combine explicit deletion with a TTL: deletion keeps data fresh, and the TTL is a safety net for anything you missed.

Protect Against Cache Stampedes

When a popular key expires, hundreds of requests can miss at once and hit the database simultaneously. Mitigations:

  • Request coalescing — only one request rebuilds the value while others wait.
  • Jittered TTLs — add randomness so keys don't expire together.
  • Stale-while-revalidate — serve the old value briefly while refreshing in the background.

Operational Tips

  • Set a memory limit and eviction policy (such as allkeys-lru) so Redis never runs out of memory.
  • Monitor hit rate; a low hit rate means the cache adds latency without benefit.
  • Don't store huge values — large objects slow the whole instance.
  • Treat Redis as a cache, not the source of truth, unless you've configured persistence deliberately.

Key Takeaways

  • Cache data that's read often, changes rarely and is expensive to produce.
  • Cache-aside with TTLs is the safest default; include the tenant in every key.
  • Invalidate on write and keep TTLs as a safety net.
  • Prevent stampedes with coalescing, jitter or stale-while-revalidate.

Sources

More posts