Difficulty: Intermediate
How do you implement caching with Redis in a Node.js application? Explain caching strategies and cache invalidation.
Redis is an in-memory data store used as a cache, session store, message broker, and rate limiter in Node.js applications. It stores data as key-value pairs with optional expiration (TTL), providing sub-millisecond read times.
The cache-aside (lazy loading) pattern is the most common: check Redis first, if cache miss then query the database, store the result in Redis with a TTL, and return it. This reduces database load significantly for frequently accessed data.
Cache invalidation is the hardest part of caching. Strategies include: TTL-based expiration (simplest), write-through (update cache on every write), and event-based invalidation (invalidate when data changes). Choose based on data freshness requirements.
Redis also supports data structures beyond strings: hashes (object fields), lists (queues), sets (unique collections), sorted sets (leaderboards), and pub/sub for real-time messaging. These make Redis useful far beyond simple caching.
const Redis = require('ioredis');
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: Number(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD,
retryStrategy: (times) => Math.min(times * 50, 2000),
});
redis.on('connect', () => console.log('Redis connected'));
redis.on('error', (err) => console.error('Redis error:', err));
// Basic operations
await redis.set('key', 'value'); // Set string
await redis.set('key', 'value', 'EX', 3600); // Set with 1hr TTL
const val = await redis.get('key'); // Get string
// JSON objects (store as string)
await redis.set('user:1', JSON.stringify({ name: 'Alice', role: 'admin' }), 'EX', 600);
const user = JSON.parse(await redis.get('user:1'));
// Hash (object fields)
await redis.hset('user:2', 'name', 'Bob', 'email', 'bob@test.com');
const name = await redis.hget('user:2', 'name');
const all = await redis.hgetall('user:2');
// Delete
await redis.del('key', 'user:1');
ioredis is the standard Redis client for Node.js. Store objects as JSON strings with TTL. Use hashes for partial updates without serializing the entire object.
async function getCachedUser(userId) {
const cacheKey = `user:${userId}`;
// 1. Check cache
const cached = await redis.get(cacheKey);
if (cached) {
console.log('Cache HIT');
return JSON.parse(cached);
}
// 2. Cache miss - query database
console.log('Cache MISS');
const user = await prisma.user.findUnique({
where: { id: userId },
include: { profile: true, posts: { take: 5 } },
});
if (!user) return null;
// 3. Store in cache with TTL
await redis.set(cacheKey, JSON.stringify(user), 'EX', 600); // 10 min
return user;
}
// Cache-aside middleware for Express
function cacheMiddleware(keyPrefix, ttl = 300) {
return async (req, res, next) => {
const key = `${keyPrefix}:${req.originalUrl}`;
const cached = await redis.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
// Override res.json to cache the response
const originalJson = res.json.bind(res);
res.json = (data) => {
redis.set(key, JSON.stringify(data), 'EX', ttl);
return originalJson(data);
};
next();
};
}
app.get('/api/products', cacheMiddleware('products', 120), getProducts);
Cache-aside: check cache first, query DB on miss, store result. The middleware pattern caches entire API responses transparently.
// Strategy 1: Invalidate on write
async function updateUser(userId, data) {
const user = await prisma.user.update({
where: { id: userId },
data,
});
// Invalidate specific cache
await redis.del(`user:${userId}`);
// Invalidate list caches that might contain this user
const keys = await redis.keys('users:list:*');
if (keys.length) await redis.del(...keys);
return user;
}
// Strategy 2: Write-through (update cache AND db)
async function updateUserWriteThrough(userId, data) {
const user = await prisma.user.update({
where: { id: userId },
data,
});
// Update cache with fresh data
await redis.set(`user:${userId}`, JSON.stringify(user), 'EX', 600);
return user;
}
// Strategy 3: Cache tags for bulk invalidation
async function invalidateTag(tag) {
const keys = await redis.smembers(`tag:${tag}`);
if (keys.length) {
await redis.del(...keys);
await redis.del(`tag:${tag}`);
}
}
// When caching, register the key under a tag
async function cacheWithTag(key, value, ttl, tags) {
await redis.set(key, JSON.stringify(value), 'EX', ttl);
for (const tag of tags) {
await redis.sadd(`tag:${tag}`, key);
}
}
Invalidate-on-write deletes stale cache. Write-through updates the cache. Tags let you invalidate groups of related keys efficiently.
Redis, Cache-Aside, TTL, Cache Invalidation, Session Store