Why Redis Caching?
Redis sits between your app and database, serving frequent reads from memory at microsecond latency instead of millisecond DB queries.
Setup
npm install ioredis
import Redis from 'ioredis'
const redis = new Redis({
host: process.env.REDIS_HOST,
port: 6379,
password: process.env.REDIS_PASSWORD,
enableReadyCheck: true,
maxRetriesPerRequest: 3,
lazyConnect: true,
})
redis.on('error', (err) => console.error('Redis error:', err))
Cache-Aside Pattern
async function getUser(id: string): Promise<User | null> {
const cacheKey = `user:${id}`
// 1. Check cache
const cached = await redis.get(cacheKey)
if (cached) {
return JSON.parse(cached)
}
// 2. Cache miss — query DB
const user = await db.users.findById(id)
if (!user) return null
// 3. Store in cache with TTL
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600) // 1 hour
return user
}
// Invalidate on update
async function updateUser(id: string, data: Partial<User>) {
const user = await db.users.update(id, data)
await redis.del(`user:${id}`) // Invalidate cache
return user
}
Cache Stampede Prevention (with Distributed Lock)
import Redlock from 'redlock'
const redlock = new Redlock([redis], {
retryCount: 10,
retryDelay: 200,
})
async function getCachedData(key: string, fetchFn: () => Promise<any>) {
const cached = await redis.get(key)
if (cached) return JSON.parse(cached)
// Acquire lock to prevent multiple fetches
const lock = await redlock.acquire([`lock:${key}`], 5000)
try {
// Double-check after acquiring lock
const recheck = await redis.get(key)
if (recheck) return JSON.parse(recheck)
const data = await fetchFn()
await redis.set(key, JSON.stringify(data), 'EX', 3600)
return data
} finally {
await lock.release()
}
}
Write-Through Pattern
async function createPost(data: CreatePostDto): Promise<Post> {
// Write to DB and cache simultaneously
const post = await db.posts.create(data)
await Promise.all([
redis.set(`post:${post.id}`, JSON.stringify(post), 'EX', 86400),
redis.lpush('posts:recent', post.id),
redis.ltrim('posts:recent', 0, 99), // Keep only 100 recent
])
return post
}
Rate Limiting
async function rateLimit(userId: string, limit = 100, windowSecs = 60): Promise<boolean> {
const key = `ratelimit:${userId}:${Math.floor(Date.now() / 1000 / windowSecs)}`
const count = await redis.incr(key)
if (count === 1) {
await redis.expire(key, windowSecs)
}
return count <= limit
}
// Express middleware
app.use(async (req, res, next) => {
const allowed = await rateLimit(req.user?.id ?? req.ip)
if (!allowed) return res.status(429).json({ error: 'Rate limit exceeded' })
next()
})
Pub/Sub for Real-Time
const publisher = new Redis()
const subscriber = new Redis()
// Publisher
await publisher.publish('notifications', JSON.stringify({
userId: '123',
message: 'Your order shipped!',
}))
// Subscriber
await subscriber.subscribe('notifications')
subscriber.on('message', (channel, message) => {
const notification = JSON.parse(message)
socketServer.to(notification.userId).emit('notification', notification)
})
Sorted Sets for Leaderboards
// Add score
await redis.zadd('leaderboard', score, userId)
// Get top 10
const top10 = await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES')
// Get user rank
const rank = await redis.zrevrank('leaderboard', userId)
Cache Key Strategy
const keys = {
user: (id: string) => `user:v1:${id}`,
userPosts: (id: string, page: number) => `user:${id}:posts:p${page}`,
product: (id: string) => `product:v1:${id}`,
search: (query: string) => `search:${Buffer.from(query).toString('base64')}`,
}
// Version your keys — increment v1 -> v2 to invalidate all cached data