Skip to content

The Cache-Aside Pattern with Redis

Aman Kumar Singh4 min read
Part 9 of 15From the Redis Masterclass series
The Cache-Aside Pattern with Redis — article by Aman Kumar Singh

Cache-aside is the caching pattern you'll use most, and probably the one you already use without knowing its name. The application checks the cache first, and on a miss, loads from the database and populates the cache itself. It's simple, it puts the application in control, and it degrades gracefully when the cache is unavailable. Understanding it well (including the two subtle bugs it invites) is the foundation for every other caching pattern in this module.

This is part 9 of the Redis Masterclass and the start of the caching module, following key expiration and TTL.

The pattern

Cache-aside (also called lazy loading) has the application manage the cache directly. On a read:

  1. Check Redis for the key.
  2. If it's there (a hit), return it.
  3. If it's not (a miss), read from the database, write the result to Redis with a TTL, and return it.
function getUser(id) {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);         // hit

  const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);  // miss
  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 300);  // populate, 5-min TTL
  return user;
}

That's the whole pattern. The cache fills lazily, only with data that's actually requested, so you never cache things nobody reads. And it's resilient: if Redis is down, every request is a "miss" that falls through to the database, so the app keeps working (slower, but working). This graceful degradation is a big reason cache-aside is the default.

Writes: invalidate, don't update

On the write side, cache-aside's standard rule is to update the database and then invalidate the cache entry, rather than updating the cache:

function updateUser(id, data) {
  await db.query('UPDATE users SET ... WHERE id = $1', [id]);
  await redis.del(`user:${id}`);   // invalidate; next read re-populates
}

Deleting the key means the next read misses and re-populates from the fresh database value. This is simpler and safer than writing the new value into the cache, because it avoids a class of races where a stale write lands in the cache after a newer one. Delete-on-write is the conventional companion to cache-aside reads.

The two bugs to know

Cache-aside is simple but invites two well-known problems.

The stale-cache race. Consider: request A reads, misses, and loads the old value from the database. Before A writes it to the cache, request B updates the database and deletes the (still empty) cache key. Then A writes its now-stale value into the cache, where it lingers until the TTL. The window is small but real under load. The main defenses are a sensible TTL (so staleness is bounded) and, where correctness demands it, more careful patterns we'll cover. For most caching, a short TTL is enough, because the wrong value self-corrects quickly.

The thundering herd (cache stampede). When a popular key expires, many concurrent requests all miss at once and all hit the database simultaneously to repopulate it, spiking load exactly when the key is hottest. This is common enough to deserve its own article later in the module. The quick mitigations are adding jitter to TTLs so keys don't expire in sync, and using a lock so only one request rebuilds the key while others wait.

Choosing what and how long to cache

Two decisions shape a cache-aside setup. What to cache: data that's read far more than written and expensive to fetch (a user's profile joined across tables, a computed dashboard, a rarely-changing config). Caching data that changes on every read, or is trivial to fetch, adds complexity for no gain.

How long (the TTL): the tradeoff is freshness against hit rate. A short TTL keeps data fresh but misses more often; a long TTL caches better but serves staler data. Match it to how stale the data can be: seconds for fast-changing data, minutes or hours for stable data. When exact freshness matters, pair a longer TTL with explicit invalidation on write, so the cache updates immediately on change and the TTL is just a backstop.

Why cache-aside is the default

Cache-aside wins by being simple and resilient. The application owns the logic, so there's no special cache infrastructure to configure. It caches only what's used. It survives cache outages by falling through to the database. And it composes with the other patterns: read-through, write-through, and the rest are essentially cache-aside with the cache logic moved to a different layer or made more sophisticated.

The pattern to internalize: check cache, on miss load and populate, on write invalidate. Add a TTL as a staleness backstop, and be aware of the stale-write race and the stampede. That covers the large majority of real caching, and everything else in this module builds on it.

Next, we look at read-through and write-through caching, which move the cache logic out of the application and into the cache layer itself, and how that changes the tradeoffs.

Key takeaways

  • Cache-aside: check Redis first; on a miss, load from the database, populate the cache with a TTL, and return.
  • It fills lazily (only requested data is cached) and degrades gracefully, since a cache outage just becomes misses that hit the database.
  • On writes, update the database and delete the cache key so the next read re-populates, rather than writing the new value in.
  • Watch two bugs: the stale-write race (bounded by a TTL) and the cache stampede when a hot key expires.
  • Cache data that's read-heavy and expensive to fetch, and set the TTL by how stale the data can safely be.

Frequently asked questions

What is the cache-aside pattern?

A caching approach where the application checks the cache first and, on a miss, loads the data from the database, stores it in the cache with a TTL, and returns it. The application manages the cache directly, filling it lazily with only the data that's requested.

Should I update or delete the cache on a write?

Delete (invalidate) the cache key and let the next read re-populate it from the database. Deleting avoids races where a stale value gets written into the cache after a newer one, which updating the cache directly can cause.

What happens if Redis goes down with cache-aside?

The application keeps working. Every read becomes a cache miss that falls through to the database, so responses are slower but correct. This graceful degradation is a key advantage of cache-aside.

What is a cache stampede?

When a popular cached key expires and many concurrent requests all miss and hit the database at once to rebuild it, spiking load. Mitigate it with TTL jitter so keys don't expire together and a lock so only one request rebuilds the key.

How do I choose a cache TTL?

Balance freshness against hit rate based on how stale the data can be: short TTLs for fast-changing data, longer for stable data. When exact freshness matters, use a longer TTL plus explicit invalidation on write so the cache updates immediately.

Related articles

Redis Caching Best Practices and Pitfalls — Aman Kumar Singh
Cache Invalidation Strategies with Redis — Aman Kumar Singh
Refresh-Ahead Caching with Redis — Aman Kumar Singh

Explore more on

Aman Kumar Singh

About the author

I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.