Redis Caching Strategies for High-Performance Applications
Our API response times dropped from 800ms to 50ms after implementing Redis properly. Here's the complete guide to caching that actually works.
Why Redis?
- In-memory storage - Microsecond latency
- Rich data structures - Not just key-value
- Persistence options - Don't lose data on restart
- Pub/Sub - Real-time messaging
Basic Operations
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
// Set and Get
await redis.set('user:123', JSON.stringify(userData));
await redis.set('session:abc', 'data', 'EX', 3600); // Expires in 1 hour
const user = JSON.parse(await redis.get('user:123'));
// Delete
await redis.del('user:123');
// Check existence
const exists = await redis.exists('user:123');
Caching Patterns
Cache-Aside (Lazy Loading)
Most common pattern:
async function getUser(userId) {
const cacheKey = `user:${userId}`;
// Check cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Cache miss - fetch from DB
const user = await db.users.findUnique({ where: { id: userId } });
// Store in cache with 1-hour TTL
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);
return user;
}
Write-Through
Update cache when writing to database:
async function updateUser(userId, data) {
// Update database
const user = await db.users.update({
where: { id: userId },
data
});
// Update cache immediately
await redis.set(`user:${userId}`, JSON.stringify(user), 'EX', 3600);
return user;
}
Cache Invalidation
async function deleteUser(userId) {
await db.users.delete({ where: { id: userId } });
// Invalidate related caches
await redis.del(`user:${userId}`);
await redis.del(`user:${userId}:posts`);
await redis.del(`user:${userId}:followers`);
}
Advanced Data Structures
Hash (for objects)
// Store user as hash (more memory efficient)
await redis.hset('user:123', {
name: 'Priya',
email: '[email protected]',
role: 'admin'
});
// Get single field
const name = await redis.hget('user:123', 'name');
// Get all fields
const user = await redis.hgetall('user:123');
Sorted Set (for leaderboards)
// Add scores
await redis.zadd('leaderboard', 1000, 'player:1');
await redis.zadd('leaderboard', 1500, 'player:2');
// Get top 10
const topPlayers = await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES');
// Get player rank
const rank = await redis.zrevrank('leaderboard', 'player:1');
List (for queues)
// Add to queue
await redis.lpush('email:queue', JSON.stringify(emailData));
// Process from queue
const email = JSON.parse(await redis.rpop('email:queue'));
Cache Warming
Pre-populate cache for known hot data:
async function warmCache() {
const popularProducts = await db.products.findMany({
where: { featured: true }
});
const pipeline = redis.pipeline();
popularProducts.forEach(product => {
pipeline.set(`product:${product.id}`, JSON.stringify(product), 'EX', 7200);
});
await pipeline.exec();
}
Avoiding Cache Stampede
When cache expires, prevent all requests from hitting DB:
async function getWithLock(key, fetchFn, ttl = 3600) {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const lockKey = `lock:${key}`;
const acquired = await redis.set(lockKey, '1', 'NX', 'EX', 10);
if (!acquired) {
// Another process is fetching, wait and retry
await new Promise(r => setTimeout(r, 100));
return getWithLock(key, fetchFn, ttl);
}
try {
const data = await fetchFn();
await redis.set(key, JSON.stringify(data), 'EX', ttl);
return data;
} finally {
await redis.del(lockKey);
}
}
Monitoring
Key metrics to watch:
- Hit rate (should be > 95%)
- Memory usage
- Connection count
- Latency
Redis is simple to start with but has depth. Master these patterns and your applications will fly.