A well-placed cache can cut database load by 90% and shave hundreds of milliseconds off responses. Redis is the default tool — but caching is easy to get subtly wrong. Here are the patterns that work.
Cache-Aside (The Default)
async function getUser(id) {
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await db.users.findById(id);
await redis.set(`user:${id}`, JSON.stringify(user), "EX", 300); // 5 min TTL
return user;
}Invalidate on Write
The hard part of caching. When data changes, delete the key so the next read repopulates it:
async function updateUser(id, data) {
await db.users.update(id, data);
await redis.del(`user:${id}`); // next read refreshes the cache
}Choosing TTLs
- Short TTLs (seconds) for fast-changing data — accept slight staleness for safety.
- Long TTLs with explicit invalidation for stable data like product catalogs.
- Add jitter to TTLs to avoid a 'thundering herd' when many keys expire at once.
Beware Stampedes
When a hot key expires under load, hundreds of requests hit the DB at once. Use a lock or 'stale-while-revalidate' so only one request rebuilds the cache.
