Keep cached data correct and your database safe — delete keys on write so reads never serve stale data, cache empty results to stop repeated misses, and prevent a cache stampede with a lock.
A cache is only as trustworthy as its invalidation. The simplest reliable rule: whenever you change the underlying data, delete the cached key. The next read misses, refetches, and re-caches the fresh value. Trying to update the cached value in place instead is where subtle stale-data bugs live — deleting is simpler and safe.
async function updateProduct(id: string, data: ProductUpdate) {
const product = await db.product.update({ where: { id }, data })
// Drop the stale cache entry. Next read rebuilds it fresh.
await redis.del('product:' + id)
return product
}
// Need to clear many related keys? Delete them together:
await redis.del('product:' + id, 'product:' + id + ':reviews')If a key is missing in the database, a naive cache-aside skips the cache write — so every request for that missing id hammers the database forever. Cache the empty result as well, but with a short TTL so a later insert shows up soon. This blunts both accidental misses and attackers probing random ids.
async function getProduct(id: string) {
const key = 'product:' + id
const cached = await redis.get(key)
if (cached) return cached === 'null' ? null : JSON.parse(cached)
const product = await db.product.findUnique({ where: { id } })
// Cache a hit for 5 min, a MISS for just 30s so new rows appear soon.
await redis.set(key, JSON.stringify(product ?? null), 'EX',
product ? 300 : 30)
return product
}When a popular key expires, many requests miss at the same instant and all rush the database together — a "cache stampede" that can knock it over. A lightweight fix: the first request grabs a short lock with set NX (set only if the key does not exist) and does the expensive load; the others briefly wait and then read the now-warm cache.
async function getWithLock(id: string) {
const key = 'product:' + id
const cached = await redis.get(key)
if (cached) return JSON.parse(cached)
// NX = only one request wins the lock; it expires in 5s as a safety net.
const gotLock = await redis.set(key + ':lock', '1', 'EX', 5, 'NX')
if (!gotLock) {
await new Promise((r) => setTimeout(r, 200)) // let the winner fill it
return getWithLock(id) // retry — likely a hit now
}
const product = await db.product.findUnique({ where: { id } })
await redis.set(key, JSON.stringify(product), 'EX', 300)
await redis.del(key + ':lock')
return product
}