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

  1. Set expiration times - Prevent memory bloat
  2. Use appropriate data structures - Hash for objects, sorted sets for rankings
  3. Implement cache warming - Preload critical data
  4. Monitor memory usage - Use INFO memory command
  5. Use connection pooling - Reuse connections
  6. Handle cache failures gracefully - Fallback to database
  7. 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.