5 Redis Caching Patterns Every Developer Should Know
Caching is the closest thing we have to a magic wand in software engineering. Add a cache, and suddenly your 500ms database query takes 2ms. But not all caching is created equal.
Over the years, I've learned that choosing the right caching pattern is just as important as choosing to cache at all. Let's explore the five patterns you'll actually use.
1. Cache-Aside (Lazy Loading)
This is the bread and butter of caching. Check the cache first, hit the database on miss, then populate the cache for next time.
async function getUser(id) {
// Check cache first
let user = await redis.get(`user:${id}`)
if (user) return JSON.parse(user)
// Cache miss - fetch and store
user = await db.users.findById(id)
await redis.setex(`user:${id}`, 3600,
JSON.stringify(user))
return user
}Use when: You have read-heavy workloads and can tolerate slightly stale data.
2. Write-Through
Write to cache and database at the same time. The cache is always consistent.
async function updateUser(id, data) {
const user = await db.users.update(id, data)
await redis.setex(`user:${id}`, 3600,
JSON.stringify(user))
return user
}Use when: You need strong consistency and can afford the write latency.
3. Write-Behind
Write to cache immediately, sync to database later. Fast writes, eventual consistency.
async function updateUser(id, data) {
await redis.setex(`user:${id}`, 3600,
JSON.stringify(data))
await queue.add('sync-db', { id, data })
return data
}When to use write-behind
4. Refresh-Ahead
Proactively refresh cache before it expires. Users never see a cache miss.
async function getUser(id) {
const ttl = await redis.ttl(`user:${id}`)
// Refresh if expiring soon
if (ttl > 0 && ttl < 300) {
refreshUserAsync(id) // Don't await
}
return redis.get(`user:${id}`)
}5. Cache Stampede Prevention
When cache expires, only one process should rebuild it. Others wait.
async function getWithLock(key, fetchFn) {
let data = await redis.get(key)
if (data) return JSON.parse(data)
const lockKey = `lock:${key}`
const locked = await redis.set(lockKey, '1',
'NX', 'EX', 10)
if (locked) {
data = await fetchFn()
await redis.setex(key, 3600,
JSON.stringify(data))
await redis.del(lockKey)
} else {
await sleep(100)
return getWithLock(key, fetchFn)
}
return data
}Which Pattern Should You Start With?
Start with Cache-Aside. It's simple, it works, and it handles most use cases. Graduate to other patterns when you have specific requirements that cache-aside doesn't meet.
Remember: the best cache is one you don't have to think about. Keep it simple.
Ready to build something amazing?
Get your Redis database running in 30 seconds. No credit card required.
Start Free →