Table of Contents
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.