Redis in Enterprise: Caching Patterns and Pitfalls
Practical patterns for using Redis in enterprise applications. Cache invalidation strategies, cluster deployment, and common anti-patterns.
Redis in Enterprise: Caching Patterns and Pitfalls
Redis is more than a cache, but caching is where most enterprises start. After implementing Redis across multiple large-scale applications—from e-commerce platforms handling 50,000 requests per second to real-time analytics dashboards—I've learned that the difference between Redis success and failure often comes down to understanding patterns and avoiding common pitfalls.
When to Cache (and When Not To)
Before diving into patterns, understand what benefits from caching:
Good Caching Candidates
- Expensive database queries: Complex joins, aggregations
- External API responses: Rate-limited third-party APIs
- Computed results: Recommendations, search rankings
- Session data: User authentication state
- Configuration: Feature flags, settings
Poor Caching Candidates
- Frequently changing data: Real-time stock prices
- User-specific data with high cardinality: Every user's unique feed
- Large objects: Videos, large files (use CDN instead)
- Data requiring strong consistency: Financial transactions
Core Caching Patterns
Cache-Aside (Lazy Loading)
The most common pattern—application manages the cache directly:
async function getUserById(userId: string): Promise<User> {
const cacheKey = `user:${userId}`;
// Try cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Cache miss - fetch from database
const user = await db.users.findById(userId);
// Store in cache for future requests
if (user) {
await redis.setex(cacheKey, 3600, JSON.stringify(user)); // 1 hour TTL
}
return user;
}
async function updateUser(userId: string, data: Partial<User>): Promise<User> {
// Update database
const user = await db.users.update(userId, data);
// Invalidate cache
await redis.del(`user:${userId}`);
return user;
}Pros: Simple, only caches what's needed
Cons: Initial requests are slow (cache miss), potential for stale data
Write-Through
Write to cache and database simultaneously:
async function updateProduct(productId: string, data: Product): Promise<Product> {
const cacheKey = `product:${productId}`;
// Update database first
const product = await db.products.update(productId, data);
// Update cache with same data
await redis.setex(cacheKey, 3600, JSON.stringify(product));
return product;
}Pros: Cache always consistent with database
Cons: Higher latency on writes, may cache data never read
Write-Behind (Write-Back)
Write to cache immediately, persist to database asynchronously:
async function recordPageView(pageId: string): Promise<void> {
const cacheKey = `pageviews:${pageId}`;
// Increment in Redis immediately
await redis.incr(cacheKey);
// Queue for database persistence
await queue.add('persist-pageviews', { pageId }, {
delay: 5000, // Batch writes every 5 seconds
removeOnComplete: true
});
}
// Background worker
async function persistPageViews(job: Job): Promise<void> {
const { pageId } = job.data;
const cacheKey = `pageviews:${pageId}`;
// Get current count and reset
const count = await redis.getset(cacheKey, '0');
if (parseInt(count) > 0) {
await db.pageViews.increment(pageId, parseInt(count));
}
}Pros: Very fast writes, handles spikes well
Cons: Risk of data loss if Redis fails before persistence
Read-Through
Cache layer handles fetching from database:
// Using a caching library with read-through support
const cache = new CacheManager({
store: redisStore,
ttl: 3600,
refreshThreshold: 300 // Refresh if TTL < 5 minutes
});
async function getProduct(productId: string): Promise<Product> {
return cache.wrap(
\