Redis Caching Patterns for High-Performance Applications

Production-friendly Redis patterns for reducing load, avoiding stampedes, and keeping cache invalidation under control.

Baikal Signal
Faster responses without turning cache behavior into folklore.

Effective caching can reduce database load by 80-90% and dramatically improve response times. This guide covers proven Redis caching patterns with production examples.

Cache-Aside Pattern

Most common pattern: application checks cache, falls back to database on miss.

async function getUser(userId) {
                                                                          // Try cache first
                                                                          const cached = await redis.get(`user:${userId}`);
                                                                          if (cached) {
                                                                            return JSON.parse(cached);
                                                                          }
                                                                        
                                                                          // Cache miss: load from database
                                                                          const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
                                                                          
                                                                          // Store in cache with TTL
                                                                          await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
                                                                          
                                                                          return user;
                                                                        }

Advantages

  • Application controls caching logic
  • Cache failures don't break application
  • Flexible TTL per cache entry

Handling Cache Stampede

When cache expires, multiple requests hit database simultaneously:

async function getUser(userId) {
                                                                          const cached = await redis.get(`user:${userId}`);
                                                                          if (cached) return JSON.parse(cached);
                                                                        
                                                                          // Try to acquire lock
                                                                          const lockKey = `lock:user:${userId}`;
                                                                          const locked = await redis.set(lockKey, '1', 'EX', 10, 'NX');
                                                                          
                                                                          if (locked) {
                                                                            try {
                                                                              // This request loads from database
                                                                              const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
                                                                              await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
                                                                              return user;
                                                                            } finally {
                                                                              await redis.del(lockKey);
                                                                            }
                                                                          } else {
                                                                            // Another request is loading, wait and retry
                                                                            await sleep(100);
                                                                            return getUser(userId);
                                                                          }
                                                                        }

Read-Through Pattern

Cache library handles database access transparently:

const cache = new Cache({
                                                                          load: async (key) => {
                                                                            const [type, id] = key.split(':');
                                                                            if (type === 'user') {
                                                                              return await db.query('SELECT * FROM users WHERE id = ?', [id]);
                                                                            }
                                                                          },
                                                                          ttl: 3600
                                                                        });
                                                                        
                                                                        // Application code doesn't see cache logic
                                                                        const user = await cache.get('user:123');

Write Patterns

Write-Through

Update cache and database together:

async function updateUser(userId, data) {
                                                                          // Update database
                                                                          await db.query('UPDATE users SET ? WHERE id = ?', [data, userId]);
                                                                          
                                                                          // Update cache immediately
                                                                          const user = { id: userId, ...data };
                                                                          await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
                                                                          
                                                                          return user;
                                                                        }

Write-Behind (Write-Back)

Write to cache immediately, persist to database asynchronously:

async function updateUser(userId, data) {
                                                                          // Update cache immediately (fast response)
                                                                          await redis.setex(`user:${userId}`, 3600, JSON.stringify(data));
                                                                          
                                                                          // Queue database write
                                                                          await queue.publish('user.update', { userId, data });
                                                                          
                                                                          return data;
                                                                        }
                                                                        
                                                                        // Background worker persists to database
                                                                        queue.subscribe('user.update', async ({ userId, data }) => {
                                                                          await db.query('UPDATE users SET ? WHERE id = ?', [data, userId]);
                                                                        });

Write-Around

Write to database, invalidate cache:

async function updateUser(userId, data) {
                                                                          await db.query('UPDATE users SET ? WHERE id = ?', [data, userId]);
                                                                          
                                                                          // Invalidate cache (next read will fetch fresh data)
                                                                          await redis.del(`user:${userId}`);
                                                                        }

TTL Strategy

Choose TTL based on data characteristics:

  • User profiles: 1 hour (infrequent changes)
  • Product catalog: 5 minutes (moderate changes)
  • Real-time prices: 10 seconds (frequent changes)
  • Session data: 24 hours with sliding expiration

Sliding Expiration

async function getSession(sessionId) {
                                                                          const session = await redis.get(`session:${sessionId}`);
                                                                          if (session) {
                                                                            // Reset TTL on access
                                                                            await redis.expire(`session:${sessionId}`, 86400);
                                                                            return JSON.parse(session);
                                                                          }
                                                                          return null;
                                                                        }

Cache Invalidation

Two hard problems in computer science: cache invalidation and naming things.

Tag-Based Invalidation

// When caching, store tags
                                                                        await redis.setex('user:123', 3600, JSON.stringify(user));
                                                                        await redis.sadd('tag:users', 'user:123');
                                                                        await redis.sadd('tag:team:5', 'user:123');
                                                                        
                                                                        // Invalidate all users
                                                                        const keys = await redis.smembers('tag:users');
                                                                        await redis.del(...keys);
                                                                        
                                                                        // Invalidate all team members
                                                                        const teamKeys = await redis.smembers('tag:team:5');
                                                                        await redis.del(...teamKeys);

Event-Based Invalidation

// Publish invalidation event
                                                                        await pubsub.publish('cache.invalidate', {
                                                                          pattern: 'user:*',
                                                                          reason: 'bulk_update'
                                                                        });
                                                                        
                                                                        // All app instances subscribe
                                                                        pubsub.subscribe('cache.invalidate', async (msg) => {
                                                                          const keys = await redis.keys(msg.pattern);
                                                                          await redis.del(...keys);
                                                                        });

Summary

Start with cache-aside for simplicity. Add cache stampede protection for hot keys. Use write-through for strong consistency, write-behind for performance, and write-around for read-heavy workloads. Choose TTL based on data update frequency. Implement tag-based invalidation for complex relationships. Monitor cache hit rates and adjust strategies accordingly.