Caching with Redis: Patterns, TTLs and Stampedes
A cache in front of a slow database query or API can cut response times from hundreds of milliseconds to one. Getting freshness and failure behaviour right is the hard part.
Cache-aside (lazy loading)
async function getProduct(id) {
const key = `cache:product:${id}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const product = await db.products.find(id); // the slow part
if (product) await redis.set(key, JSON.stringify(product), { EX: 600 });
return product;
}
The app checks the cache first, and on a miss loads from the database and fills the cache.
Invalidate on write
async function updateProduct(id, changes) {
await db.products.update(id, changes);
await redis.del(`cache:product:${id}`); // next read reloads fresh data
}
Deleting is safer than writing the new value into the cache, which can race with concurrent updates.
Choosing TTLs
- Always set one; it's your safety net if invalidation is missed.
- Short (seconds to minutes) for data that changes often, longer for reference data.
- Add random jitter (for example 600 ± 60 seconds) so many keys don't expire at the same moment.
Preventing a stampede
When a popular key expires, hundreds of requests can miss at once and all hit the database. Let only one rebuild it:
async function getWithLock(key, ttl, load) {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const gotLock = await redis.set(`${key}:lock`, '1', { NX: true, PX: 5000 });
if (!gotLock) {
await new Promise((r) => setTimeout(r, 100)); // someone else is rebuilding
return getWithLock(key, ttl, load);
}
try {
const value = await load();
await redis.set(key, JSON.stringify(value), { EX: ttl });
return value;
} finally {
await redis.del(`${key}:lock`);
}
}
Fail open
If Redis is down, fall back to the database (with timeouts) rather than failing the request. A cache should make things faster, not become a single point of failure.
What not to cache
- Data that must be exactly current (account balances, stock at checkout).
- Per-user data with huge key counts and low reuse.
- Anything cheap to compute; measure before adding a cache.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.