Redis for Caching: Boost Your Application Performance
Redis is an in-memory data structure store that excels as a cache, message broker, and real-time data platform. Learn how to leverage Redis for lightning-fast application performance.
What is Redis?
Redis (Remote Dictionary Server) stores data in memory, making it incredibly fast with sub-millisecond response times. It’s perfect for caching, session management, and real-time analytics.
Key Features:
- In-memory storage - Microsecond latency
- Rich data structures - Strings, lists, sets, hashes, sorted sets
- Persistence options - Optional disk snapshots
- Pub/Sub messaging - Real-time event streaming
- Atomic operations - Built-in transactions
Installation
Using Docker:
docker run -d --name redis -p 6379:6379 redis:alpine
macOS:
brew install redis
brew services start redis
Node.js Client:
npm install redis
Basic Operations
Connect and use:
const redis = require('redis');
const client = redis.createClient({
host: 'localhost',
port: 6379
});
await client.connect();
// String operations
await client.set('user:1:name', 'Alice Johnson');
const name = await client.get('user:1:name');
console.log(name); // "Alice Johnson"
// Set with expiration (in seconds)
await client.setEx('session:abc123', 3600, 'user-data');
// Increment counter
await client.incr('page:views');
await client.incrBy('page:views', 10);
// Multiple keys
await client.mSet({
'user:1:name': 'Alice',
'user:1:email': 'alice@example.com'
});
const values = await client.mGet(['user:1:name', 'user:1:email']);
Caching Strategies
Cache-Aside Pattern:
async function getUser(userId) {
const cacheKey = `user:${userId}`;
// Try cache first
const cached = await client.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Cache miss - fetch from database
const user = await db.users.findById(userId);
// Store in cache (expire in 1 hour)
await client.setEx(cacheKey, 3600, JSON.stringify(user));
return user;
}
Write-Through Cache:
async function updateUser(userId, data) {
// Update database
const user = await db.users.findByIdAndUpdate(userId, data);
// Update cache immediately
const cacheKey = `user:${userId}`;
await client.setEx(cacheKey, 3600, JSON.stringify(user));
return user;
}
Cache Invalidation:
async function deleteUser(userId) {
// Delete from database
await db.users.findByIdAndDelete(userId);
// Invalidate cache
await client.del(`user:${userId}`);
}
// Pattern-based deletion
await client.del(await client.keys('user:*:sessions'));
Hash Data Structure
Perfect for objects:
// Store user as hash
await client.hSet('user:1', {
name: 'Alice Johnson',
email: 'alice@example.com',
age: '28'
});
// Get single field
const name = await client.hGet('user:1', 'name');
// Get all fields
const user = await client.hGetAll('user:1');
// Update specific field
await client.hSet('user:1', 'age', '29');
// Increment numeric field
await client.hIncrBy('user:1', 'loginCount', 1);
// Check field exists
const exists = await client.hExists('user:1', 'email');
// Delete field
await client.hDel('user:1', 'age');
Lists for Queues
// Push to queue (left side)
await client.lPush('jobs', JSON.stringify({
type: 'email',
to: 'user@example.com'
}));
// Pop from queue (right side) - FIFO
const job = await client.rPop('jobs');
// Blocking pop (wait for item)
const job = await client.brPop('jobs', 5); // 5 second timeout
// Get list length
const length = await client.lLen('jobs');
// Get range of items
const jobs = await client.lRange('jobs', 0, 9); // First 10 items
// Trim list (keep only range)
await client.lTrim('jobs', 0, 99); // Keep only first 100
Sets for Unique Collections
// Add members
await client.sAdd('tags:post:1', ['nodejs', 'redis', 'caching']);
// Check membership
const isMember = await client.sIsMember('tags:post:1', 'nodejs');
// Get all members
const tags = await client.sMembers('tags:post:1');
// Remove member
await client.sRem('tags:post:1', 'nodejs');
// Set operations
await client.sAdd('set1', ['a', 'b', 'c']);
await client.sAdd('set2', ['b', 'c', 'd']);
const intersection = await client.sInter(['set1', 'set2']); // ['b', 'c']
const union = await client.sUnion(['set1', 'set2']); // ['a', 'b', 'c', 'd']
const diff = await client.sDiff(['set1', 'set2']); // ['a']
Sorted Sets for Rankings
// Add members with scores
await client.zAdd('leaderboard', [
{ score: 100, value: 'player1' },
{ score: 95, value: 'player2' },
{ score: 87, value: 'player3' }
]);
// Get top players (highest scores)
const top3 = await client.zRange('leaderboard', 0, 2, { REV: true });
// Get rank (position)
const rank = await client.zRevRank('leaderboard', 'player1');
// Get score
const score = await client.zScore('leaderboard', 'player1');
// Increment score
await client.zIncrBy('leaderboard', 10, 'player3');
// Range by score
const players = await client.zRangeByScore('leaderboard', 80, 100);
// Count in score range
const count = await client.zCount('leaderboard', 80, 100);
Session Management
const session = require('express-session');
const RedisStore = require('connect-redis').default;
app.use(session({
store: new RedisStore({ client }),
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Session is automatically stored in Redis
app.get('/dashboard', (req, res) => {
if (req.session.userId) {
res.send('Welcome back!');
} else {
res.redirect('/login');
}
});
Rate Limiting
async function checkRateLimit(userId, maxRequests = 100, window = 60) {
const key = `rate:${userId}`;
const current = await client.incr(key);
if (current === 1) {
// First request - set expiration
await client.expire(key, window);
}
if (current > maxRequests) {
return { allowed: false, remaining: 0 };
}
return { allowed: true, remaining: maxRequests - current };
}
// Express middleware
app.use(async (req, res, next) => {
const userId = req.user?.id || req.ip;
const { allowed, remaining } = await checkRateLimit(userId);
res.set('X-RateLimit-Remaining', remaining);
if (!allowed) {
return res.status(429).json({ error: 'Too many requests' });
}
next();
});
Pub/Sub Messaging
// Publisher
const publisher = redis.createClient();
await publisher.connect();
await publisher.publish('notifications', JSON.stringify({
type: 'new_message',
userId: 123,
message: 'Hello!'
}));
// Subscriber
const subscriber = redis.createClient();
await subscriber.connect();
await subscriber.subscribe('notifications', (message) => {
const data = JSON.parse(message);
console.log('Received:', data);
});
// Pattern subscribe
await subscriber.pSubscribe('user:*', (message, channel) => {
console.log(`Message from ${channel}:`, message);
});
Transactions
// Multi/exec for atomic operations
const multi = client.multi();
multi.incr('user:1:posts');
multi.hIncrBy('user:1', 'score', 10);
multi.sAdd('active:users', '1');
const results = await multi.exec();
// Watch for optimistic locking
await client.watch('balance:1');
const balance = await client.get('balance:1');
if (balance < 100) {
await client.unwatch();
throw new Error('Insufficient funds');
}
const multi = client.multi();
multi.decrBy('balance:1', 100);
multi.incrBy('balance:2', 100);
await multi.exec();
Caching API Responses
const express = require('express');
const app = express();
// Cache middleware
function cacheMiddleware(duration = 300) {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
const cached = await client.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
// Override res.json to cache response
const originalJson = res.json.bind(res);
res.json = (data) => {
client.setEx(key, duration, JSON.stringify(data));
return originalJson(data);
};
next();
};
}
// Use cache
app.get('/api/products', cacheMiddleware(600), async (req, res) => {
const products = await db.products.find();
res.json(products);
});
Best Practices
- Set expiration times - Prevent memory bloat
- Use appropriate data structures - Hash for objects, sorted sets for rankings
- Implement cache warming - Preload critical data
- Monitor memory usage - Use
INFO memorycommand - Use connection pooling - Reuse connections
- Handle cache failures gracefully - Fallback to database
- Use Redis Cluster for horizontal scaling
Common Patterns
Cache-with-mutex (prevent thundering herd):
async function getCachedOrFetch(key, fetchFn, ttl = 3600) {
const cached = await client.get(key);
if (cached) return JSON.parse(cached);
const lockKey = `lock:${key}`;
const locked = await client.set(lockKey, '1', {
NX: true,
EX: 10
});
if (!locked) {
// Wait and retry
await new Promise(r => setTimeout(r, 100));
return getCachedOrFetch(key, fetchFn, ttl);
}
try {
const data = await fetchFn();
await client.setEx(key, ttl, JSON.stringify(data));
return data;
} finally {
await client.del(lockKey);
}
}
Redis is essential for high-performance applications. Start with simple caching, understand data structures, and leverage advanced features like pub/sub and transactions as your needs grow.