Skip to content

Refresh-Ahead Caching with Redis

Aman Kumar Singh4 min read
Part 12 of 15From the Redis Masterclass series
Refresh-Ahead Caching with Redis — article by Aman Kumar Singh

Every cache pattern so far accepts that cache misses happen: a key expires, a request misses, and someone pays the cost of reloading from the database. Refresh-ahead attacks that directly. Instead of waiting for a hot key to expire and then reloading it (while a user waits), it refreshes the key in the background before it expires, so the cache is always warm for the data that matters. It's a more advanced pattern, worth it for a specific case: predictable, frequently-accessed data where you never want a user to eat a cache miss.

This is part 12 of the Redis Masterclass, following write-behind.

The problem with expire-then-reload

In cache-aside, a cached key lives until its TTL, then expires. The next request misses and has to reload from the database synchronously, so that unlucky request is slow. For most data that's fine, the occasional slow request is a reasonable price. But for very hot keys (a homepage's featured content, a popular product, a config every request reads) that periodic slow reload happens constantly, and each time a real user waits for the database. Worse, if many requests hit the expired key at once, they all reload together, a stampede.

Refresh-ahead's insight: for data you know will be requested again, don't wait for it to expire. Refresh it proactively while it's still valid, so requests always hit a warm cache.

How refresh-ahead works

The pattern refreshes a key before its TTL runs out, in the background, so the served value is never stale enough to expire under a user. There are a couple of ways to implement it.

The common application-level approach: when serving a cached value, check how close it is to expiring, and if it's within a refresh threshold, trigger an asynchronous refresh while still returning the current (still-valid) value:

async function getWithRefreshAhead(key, ttl, refreshThreshold) {
  const remaining = await redis.ttl(key);
  const value = await redis.get(key);

  if (value && remaining < refreshThreshold) {
    // still valid, but expiring soon: refresh in the background, don't wait
    queueRefresh(key, ttl);
  }
  if (value) return JSON.parse(value);      // return current value immediately

  return await loadAndCache(key, ttl);       // cold miss fallback
}

The user always gets an immediate response from the warm cache, and the refresh happens out of band. The key's value is renewed before it would have expired, so a true miss (with its synchronous reload) rarely happens for hot keys.

The other approach is scheduled: a background job periodically refreshes a known set of hot keys on a timer, independent of requests. This suits a fixed set of critical keys (site config, featured lists) that you want kept warm regardless of traffic.

What it costs

Refresh-ahead isn't free, and the costs explain when it's worth it. You refresh keys that might not have been requested again, doing work speculatively, so you spend database queries on data no one may need. And it's more complex: you need the background refresh mechanism, the threshold logic, and care to avoid refreshing the same key many times at once.

That speculative-work cost is why refresh-ahead is targeted, not a default. Applying it to every key would flood the database with proactive refreshes for cold data. It pays off only for keys that are genuinely hot and predictable, where the refresh work is almost always useful because the key really will be requested again.

When to use it

Refresh-ahead fits a narrow, valuable case:

  • Hot keys accessed constantly: content, config, or data on the critical path of most requests, where a miss would hurt many users.
  • Predictable access: data you can confidently say will be requested again soon, so the proactive refresh isn't wasted.
  • Expensive-to-compute values: when the reload is costly (a heavy query or computation), avoiding a synchronous miss is especially worthwhile.

It's overkill for the long tail of rarely-accessed keys, where cache-aside's load-on-miss is cheaper overall, because most of those keys won't be requested again before expiring anyway.

Combining with the other patterns

Refresh-ahead isn't a replacement for cache-aside, it's a targeted enhancement on top of it. A typical setup uses cache-aside for the bulk of the cache and adds refresh-ahead for a small set of identified hot keys. You get cache-aside's simplicity and graceful degradation everywhere, plus refresh-ahead's miss-avoidance where it matters most. It also directly helps with the stampede problem: proactively refreshing a hot key before it expires means it never expires under load, so the herd of simultaneous misses never forms.

Refresh-ahead is the pattern for keeping specific hot data perpetually warm. Refresh keys in the background before they expire so users never wait on a reload, accept that you're doing speculative work, and apply it only to the predictable, high-traffic keys where that work pays off. For those keys, it turns periodic slow reloads into consistently fast responses.

That covers the core caching patterns. Next, we tackle the problem every one of them shares: cache invalidation, the genuinely hard part of caching, and the strategies for keeping cached data correct.

Key takeaways

  • Refresh-ahead refreshes hot keys in the background before they expire, so users never pay for a synchronous reload.
  • Implement it by refreshing when a served key is within a threshold of expiring, or by scheduling refreshes for known hot keys.
  • The cost is speculative work: you refresh keys that might not be requested again, so it only pays off for predictable hot data.
  • Apply it to a small set of hot, predictable, expensive-to-compute keys, not to the long tail of rarely-accessed data.
  • It layers on top of cache-aside and helps prevent stampedes by ensuring hot keys never expire under load.

Frequently asked questions

What is refresh-ahead caching?

A pattern that proactively refreshes cached keys in the background before they expire, so frequently-accessed data stays warm and requests never hit a synchronous reload. It trades some speculative work for avoiding cache misses on hot keys.

How is refresh-ahead different from cache-aside?

Cache-aside reloads a key only after it expires and a request misses, so that request is slow. Refresh-ahead reloads hot keys before they expire, in the background, so users always hit a warm cache. Refresh-ahead usually layers on top of cache-aside for select keys.

When should I use refresh-ahead?

For a small set of hot, predictable keys on the critical path (site config, featured content, popular items), especially when reloading them is expensive. It's not worth it for rarely-accessed keys, where the proactive refresh work would be wasted.

What's the downside of refresh-ahead?

It does speculative work, refreshing keys that might not be requested again, which wastes database queries if applied to cold data. It's also more complex, needing background refresh logic and care to avoid redundant refreshes.

Does refresh-ahead help with cache stampedes?

Yes. By refreshing a hot key before it expires, the key never reaches an expired state under load, so the many-simultaneous-misses stampede that happens when a popular key expires doesn't form.

Related articles

Redis Caching Best Practices and Pitfalls — Aman Kumar Singh
The Cache-Aside Pattern with Redis — Aman Kumar Singh
Redis Data Structures: An Overview — 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.