nodejs4 Min Read

Redis — Caching, Sessions, Pub/Sub in Node.js

Gorav Singal

April 02, 2026

TL;DR

Redis excels as a cache (TTL-based eviction), session store (connect-redis), pub/sub broker (real-time events), and rate limiter (sliding window). Use ioredis for cluster support and Lua scripting for atomic operations.

Redis — Caching, Sessions, Pub/Sub in Node.js

Why Redis for Node.js

Redis is an in-memory data store that serves as a cache, message broker, session store, and more. Its sub-millisecond latency makes it the go-to choice for performance-critical Node.js applications.

Setting Up ioredis

const Redis = require('ioredis');

const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: 6379,
  password: process.env.REDIS_PASSWORD,
  maxRetriesPerRequest: 3,
  retryStrategy(times) {
    const delay = Math.min(times * 50, 2000);
    return delay;
  },
});

redis.on('connect', () => console.log('Redis connected'));
redis.on('error', (err) => console.error('Redis error:', err));

Redis Data Structures

// Strings — simple key-value
await redis.set('user:1:name', 'Alice');
await redis.get('user:1:name'); // 'Alice'
await redis.set('api:token', 'abc123', 'EX', 3600); // Expires in 1 hour

// Hashes — object-like storage
await redis.hset('user:1', { name: 'Alice', email: '[email protected]', role: 'admin' });
await redis.hget('user:1', 'name');    // 'Alice'
await redis.hgetall('user:1');          // { name: 'Alice', email: '...', role: 'admin' }

// Lists — ordered collection (queue/stack)
await redis.lpush('queue:emails', JSON.stringify({ to: '[email protected]', subject: 'Welcome' }));
await redis.rpop('queue:emails'); // FIFO processing

// Sets — unique values
await redis.sadd('user:1:roles', 'admin', 'editor');
await redis.sismember('user:1:roles', 'admin'); // 1 (true)
await redis.smembers('user:1:roles'); // ['admin', 'editor']

// Sorted Sets — ranked data
await redis.zadd('leaderboard', 1500, 'player:1', 2200, 'player:2', 1800, 'player:3');
await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES'); // Top 10

Caching Strategies

Cache-Aside (Lazy Loading)

The most common caching pattern. Check cache first, fall back to database, then populate cache.

Cache-Aside Pattern

class CacheService {
  constructor(redis, defaultTTL = 300) {
    this.redis = redis;
    this.defaultTTL = defaultTTL;
  }

  async get(key) {
    const cached = await this.redis.get(key);
    return cached ? JSON.parse(cached) : null;
  }

  async set(key, value, ttl = this.defaultTTL) {
    await this.redis.set(key, JSON.stringify(value), 'EX', ttl);
  }

  async invalidate(key) {
    await this.redis.del(key);
  }

  async invalidatePattern(pattern) {
    const keys = await this.redis.keys(pattern);
    if (keys.length > 0) {
      await this.redis.del(...keys);
    }
  }
}

// Usage in a service
async function getUser(id) {
  const cacheKey = `user:${id}`;

  // 1. Check cache
  const cached = await cache.get(cacheKey);
  if (cached) return cached;

  // 2. Cache miss — query database
  const user = await db.users.findById(id);
  if (!user) return null;

  // 3. Store in cache
  await cache.set(cacheKey, user, 600); // 10 min TTL

  return user;
}

// Invalidate on update
async function updateUser(id, data) {
  const user = await db.users.update(id, data);
  await cache.invalidate(`user:${id}`);
  return user;
}

Write-Through Cache

async function createOrder(orderData) {
  // Write to DB first
  const order = await db.orders.create(orderData);

  // Write to cache simultaneously
  await cache.set(`order:${order.id}`, order, 3600);

  return order;
}

Session Storage with connect-redis

const session = require('express-session');
const RedisStore = require('connect-redis').default;

app.use(session({
  store: new RedisStore({
    client: redis,
    prefix: 'sess:',
    ttl: 86400, // 24 hours
  }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000,
    sameSite: 'strict',
  },
}));

Pub/Sub Implementation

Redis Pub/Sub enables real-time messaging between services.

Redis Pub/Sub Architecture

// Publisher (uses main redis connection)
async function publishEvent(channel, data) {
  await redis.publish(channel, JSON.stringify({
    ...data,
    timestamp: Date.now(),
  }));
}

// On order creation
await publishEvent('order:created', {
  orderId: order.id,
  userId: order.userId,
  total: order.total,
});

// Subscriber (needs a DEDICATED connection)
const subscriber = new Redis(/* same config */);

subscriber.subscribe('order:created', 'user:registered', 'payment:completed');

subscriber.on('message', (channel, message) => {
  const data = JSON.parse(message);

  switch (channel) {
    case 'order:created':
      emailService.sendOrderConfirmation(data);
      analyticsService.trackOrder(data);
      break;
    case 'user:registered':
      emailService.sendWelcome(data);
      break;
    case 'payment:completed':
      fulfillmentService.processOrder(data);
      break;
  }
});

// Pattern-based subscription
subscriber.psubscribe('order:*');
subscriber.on('pmessage', (pattern, channel, message) => {
  console.log(`${channel}: ${message}`);
});

Rate Limiting with Redis

Rate Limiting

// Sliding window rate limiter using sorted sets
async function rateLimit(key, limit, windowSeconds) {
  const now = Date.now();
  const windowStart = now - windowSeconds * 1000;

  const pipeline = redis.pipeline();

  // Remove expired entries
  pipeline.zremrangebyscore(key, 0, windowStart);
  // Add current request
  pipeline.zadd(key, now, `${now}:${Math.random()}`);
  // Count requests in window
  pipeline.zcard(key);
  // Set TTL on the key
  pipeline.expire(key, windowSeconds);

  const results = await pipeline.exec();
  const requestCount = results[2][1];

  return {
    allowed: requestCount <= limit,
    remaining: Math.max(0, limit - requestCount),
    resetAt: new Date(now + windowSeconds * 1000),
  };
}

// Express middleware
function rateLimitMiddleware(limit = 100, windowSeconds = 60) {
  return async (req, res, next) => {
    const key = `ratelimit:${req.ip}`;
    const result = await rateLimit(key, limit, windowSeconds);

    res.set({
      'X-RateLimit-Limit': limit,
      'X-RateLimit-Remaining': result.remaining,
      'X-RateLimit-Reset': result.resetAt.toISOString(),
    });

    if (!result.allowed) {
      return res.status(429).json({
        error: 'Too many requests',
        retryAfter: windowSeconds,
      });
    }

    next();
  };
}

app.use('/api/', rateLimitMiddleware(100, 60));

Distributed Locks with Redlock

const Redlock = require('redlock');

const redlock = new Redlock([redis], {
  retryCount: 3,
  retryDelay: 200,
  retryJitter: 200,
});

// Acquire lock before critical section
async function processPayment(orderId) {
  const lockKey = `lock:payment:${orderId}`;

  const lock = await redlock.acquire([lockKey], 10000); // 10s lock

  try {
    // Critical section — only one process can execute this
    const order = await db.orders.findById(orderId);

    if (order.status !== 'pending') {
      throw new Error('Order already processed');
    }

    await paymentGateway.charge(order);
    await db.orders.update(orderId, { status: 'paid' });
  } finally {
    await lock.release();
  }
}

Performance Tips

// Use pipelines for batch operations (reduces round trips)
const pipeline = redis.pipeline();
for (const id of userIds) {
  pipeline.hgetall(`user:${id}`);
}
const results = await pipeline.exec();
const users = results.map(([err, data]) => data);

// Use Lua scripts for atomic operations
const luaScript = `
  local current = redis.call('GET', KEYS[1])
  if current and tonumber(current) >= tonumber(ARGV[1]) then
    return redis.call('DECRBY', KEYS[1], ARGV[1])
  end
  return nil
`;

// Atomic "decrement if sufficient balance"
const newBalance = await redis.eval(luaScript, 1, 'balance:user:1', 50);

Key Takeaways

  1. Cache-aside is the safest pattern — cache on miss, invalidate on write
  2. Pub/Sub is fire-and-forget — no persistence, use streams for durability
  3. Rate limiting with sorted sets gives precise sliding windows
  4. Use pipelines for batch operations and Lua scripts for atomicity
  5. Always set TTL on cached keys to prevent stale data
  6. Use dedicated connections for pub/sub subscribers
Share

Related Posts

WebSockets with Socket.io in Node.js

WebSockets with Socket.io in Node.js

WebSocket vs HTTP Traditional HTTP follows a request/response model — the client…

Testing Node.js — Unit, Integration, and E2E

Testing Node.js — Unit, Integration, and E2E

Testing Strategy A solid testing strategy follows the testing pyramid — many…

Database Integration — PostgreSQL with Node.js

Database Integration — PostgreSQL with Node.js

Choosing Your PostgreSQL Client Node.js has three main approaches to working…

Performance Optimization and Profiling in Node.js

Performance Optimization and Profiling in Node.js

Profiling First, Optimize Second Never optimize blindly. Always profile to find…

Node.js Architecture — Event Loop Deep Dive

Node.js Architecture — Event Loop Deep Dive

Why the Event Loop Matters Node.js runs JavaScript on a single thread, yet…

MongoDB with Mongoose — Patterns and Pitfalls

MongoDB with Mongoose — Patterns and Pitfalls

Schema Design Philosophy MongoDB schema design is fundamentally different from…

Latest Posts

AI Video Generation in 2025 — Models, Costs, and How to Build a Cost-Effective Pipeline

AI Video Generation in 2025 — Models, Costs, and How to Build a Cost-Effective Pipeline

AI video generation went from “cool demo” to “usable in production” in 2024-202…

AI Models in 2025 — Cost, Capabilities, and Which One to Use

AI Models in 2025 — Cost, Capabilities, and Which One to Use

Choosing the right AI model is one of the most impactful decisions you’ll make…

AI Image Generation in 2025 — Models, Costs, and How to Optimize Spend

AI Image Generation in 2025 — Models, Costs, and How to Optimize Spend

Generating one image with AI costs between $0.002 and $0.12. That might sound…

AI Coding Assistants in 2025 — Every Tool Compared, and Which One to Actually Use

AI Coding Assistants in 2025 — Every Tool Compared, and Which One to Actually Use

Two years ago, AI coding meant one thing: GitHub Copilot autocompleting your…

AI Agents Demystified — It's Just Automation With a Better Brain

AI Agents Demystified — It's Just Automation With a Better Brain

Let’s cut through the noise. If you read Twitter or LinkedIn, you’d think “AI…

Supply Chain Security — Protecting Your Software Pipeline

Supply Chain Security — Protecting Your Software Pipeline

In 2024, a single malicious contributor nearly compromised every Linux system on…