Redis Cache Patterns for High-Performance Applications
How to implement cache-aside, write-through, and write-behind patterns with Redis to reduce database load and improve response times
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Redis Cache Patterns for High-Performance Applications
Redis is an in-memory data structure store that serves as an extremely fast cache layer between your application and persistent database. Choosing the right caching pattern — cache-aside, write-through, or write-behind — determines how your application handles cache misses, consistency, and failure scenarios.
When to Use This
- Database queries are slow and return frequently accessed data
- You need to reduce load on primary databases during traffic spikes
- Temporary data staleness is acceptable in exchange for lower latency
Prerequisites
- Redis server running locally or via a managed service
- A client library like
ioredisorredisfor Node.js
Solution
1. Cache-Aside (Lazy Loading)
The application checks the cache first. On a miss, it loads from the database and populates the cache.
// cache/CacheAside.ts
import Redis from 'ioredis';
class CacheAsideProductRepository {
private redis = new Redis();
private ttl = 300; // 5 minutes
async getProduct(id: string): Promise<Product | null> {
const cacheKey = `product:${id}`;
// Check cache first
const cached = await this.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Cache miss: load from database
const product = await this.db.query('SELECT * FROM products WHERE id = $1', [id]);
if (!product) return null;
// Populate cache
await this.redis.setex(cacheKey, this.ttl, JSON.stringify(product));
return product;
}
async updateProduct(id: string, data: Partial<Product>): Promise<void> {
await this.db.query('UPDATE products SET ... WHERE id = $1', [id]);
// Invalidate cache to prevent stale reads
await this.redis.del(`product:${id}`);
}
}
2. Write-Through
Data is written to both cache and database simultaneously. The cache always holds the latest data.
// cache/WriteThrough.ts
class WriteThroughProductRepository {
async updateProduct(id: string, data: Partial<Product>): Promise<void> {
const cacheKey = `product:${id}`;
// Start database transaction
await this.db.query('BEGIN');
try {
await this.db.query('UPDATE products SET ... WHERE id = $1', [id]);
// Write to cache within the same logical operation
const updated = await this.db.query('SELECT * FROM products WHERE id = $1', [id]);
await this.redis.setex(cacheKey, this.ttl, JSON.stringify(updated));
await this.db.query('COMMIT');
} catch (error) {
await this.db.query('ROLLBACK');
throw error;
}
}
}
3. Write-Behind (Write-Back)
Data is written to cache first and asynchronously flushed to the database. Highest performance but riskiest.
// cache/WriteBehind.ts
class WriteBehindProductRepository {
async updateProduct(id: string, data: Partial<Product>): Promise<void> {
const cacheKey = `product:${id}`;
// Write to cache immediately
await this.redis.setex(cacheKey, this.ttl, JSON.stringify(data));
// Queue for async persistence
await this.redis.lpush('pending_writes', JSON.stringify({ id, data, timestamp: Date.now() }));
}
}
// Background worker
async function flushPendingWrites() {
const batch = await redis.lpop('pending_writes', 100);
if (!batch) return;
const writes = batch.map(item => JSON.parse(item));
await db.query('BEGIN');
try {
for (const write of writes) {
await db.query('UPDATE products SET ... WHERE id = $1', [write.id]);
}
await db.query('COMMIT');
} catch (error) {
await db.query('ROLLBACK');
// Re-queue failed writes
for (const write of writes) {
await redis.rpush('pending_writes', JSON.stringify(write));
}
}
}
// Run every 5 seconds
setInterval(flushPendingWrites, 5000);
4. Cache Stampede Prevention
// cache/StampedeProtection.ts
class StampedeProtectedCache {
async getProduct(id: string): Promise<Product> {
const cacheKey = `product:${id}`;
const lockKey = `lock:${id}`;
const cached = await this.redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// Try to acquire lock
const lock = await this.redis.set(lockKey, '1', 'EX', 10, 'NX');
if (lock) {
// We won the race; load from DB
const product = await this.db.query('SELECT * FROM products WHERE id = $1', [id]);
await this.redis.setex(cacheKey, this.ttl, JSON.stringify(product));
await this.redis.del(lockKey);
return product;
}
// Wait for the winner to populate cache
await new Promise(resolve => setTimeout(resolve, 100));
return this.getProduct(id);
}
}
How It Works
- Cache-Aside minimizes cache writes but allows brief stale data after updates
- Write-Through guarantees consistency at the cost of higher write latency
- Write-Behind maximizes throughput but risks data loss if the cache fails before flush
- Stampede Protection prevents multiple simultaneous database queries on cache expiration
Production Considerations
- Use Redis Cluster or Redis Sentinel for high availability
- Implement circuit breaker logic when Redis is unavailable; fall back to database
- Set appropriate TTL values based on data change frequency
- Monitor cache hit ratio with
INFO statsand adjust TTL accordingly
Common Mistakes
- Not handling Redis connection failures gracefully
- Using the same TTL for all data types regardless of change frequency
- Forgetting to invalidate related cache entries on updates
FAQ
Q: Which pattern should I use? A: Cache-aside for read-heavy workloads. Write-through when consistency is critical. Write-behind only when you can tolerate brief data loss.
Q: How do I handle cache invalidation across multiple services? A: Use Redis Pub/Sub or a message queue to broadcast invalidation events to all service instances.
Q: Should I compress cached data?
A: For large objects (>1KB), yes. Use msgpack or JSON compression to reduce memory usage and network transfer.
Related Resources
Implement Cache Invalidation Strategies
How to keep caches consistent with databases using TTL, write-through, write-behind, and event-driven invalidation patterns.
RecipeSet Up Connection Pooling for Databases and HTTP Clients
How to set up connection pooling for databases and HTTP clients to improve performance and reliability
GuideWeb Performance Optimization Guide
A comprehensive guide to optimizing web application performance for better Core Web Vitals and user experience.