Back to Blog
guideJanuary 28, 20268 min read

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

This pattern shines for high-write scenarios like analytics, activity feeds, or game leaderboards where occasional data loss is acceptable.

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 →