Redis with Node.js — Caching, Sessions, and Pub/Sub 2026

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Introduction

Why This Matters

Redis is the Swiss Army knife of backend infrastructure. It handles caching (sub-millisecond reads), distributed sessions (shared across Node.js instances), rate limiting (atomic increments), real-time pub/sub messaging, job queues, and leaderboards — all with a single dependency. In 2026, Redis (or its drop-in compatible cloud equivalents like Valkey, DragonflyDB, and AWS ElastiCache) is present in virtually every production Node.js architecture.

Installation and Connection

# Official Redis client (Node.js 16+)
npm install redis
 
# Or ioredis (more features, slightly different API)
npm install ioredis
// src/redis/client.ts — official redis package
import { createClient, RedisClientType } from 'redis';
 
let redisClient: RedisClientType;
 
export async function getRedisClient(): Promise<RedisClientType> {
  if (redisClient) return redisClient;
 
  redisClient = createClient({
    url: process.env.REDIS_URL ?? 'redis://localhost:6379',
    socket: {
      reconnectStrategy: (retries) => Math.min(retries * 100, 3000),
      connectTimeout: 5000,
    },
  });
 
  redisClient.on('error', (err) => console.error('Redis error:', err));
  redisClient.on('connect', () => console.log('Redis connected'));
  redisClient.on('reconnecting', () => console.log('Redis reconnecting...'));
 
  await redisClient.connect();
  return redisClient;
}
 
// Graceful shutdown
process.on('SIGTERM', async () => {
  if (redisClient) await redisClient.quit();
});

Caching Patterns

Cache-Aside (Lazy Loading)

// src/cache/users.ts
import { getRedisClient } from '../redis/client';
 
const TTL = 300; // 5 minutes
 
export async function getCachedUser(userId: string) {
  const redis = await getRedisClient();
  const cacheKey = `user:${userId}`;
 
  // Check cache first
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }
 
  // Cache miss — fetch from DB
  const user = await db.users.findById(userId);
  if (!user) return null;
 
  // Store in cache with TTL
  await redis.setEx(cacheKey, TTL, JSON.stringify(user));
  return user;
}
 
export async function invalidateUserCache(userId: string) {
  const redis = await getRedisClient();
  await redis.del(`user:${userId}`);
}

Write-Through Cache

// Write to both DB and cache on update
export async function updateUser(
  userId: string,
  data: { name?: string; email?: string }
) {
  const redis = await getRedisClient();
 
  const updatedUser = await db.users.update(userId, data);
 
  // Immediately update cache with fresh data
  await redis.setEx(`user:${userId}`, TTL, JSON.stringify(updatedUser));
 
  return updatedUser;
}

Memoize with Hash

For objects with multiple fields, Redis hashes are more efficient than serializing to JSON.

export async function getUserProfile(userId: string) {
  const redis = await getRedisClient();
  const key = `profile:${userId}`;
 
  const cached = await redis.hGetAll(key);
  if (Object.keys(cached).length > 0) {
    return cached; // Redis hashes return Record<string, string>
  }
 
  const profile = await db.profiles.findByUserId(userId);
  if (!profile) return null;
 
  await redis.hSet(key, {
    id: String(profile.id),
    bio: profile.bio,
    userId: String(profile.userId),
  });
  await redis.expire(key, TTL);
 
  return profile;
}

Session Management

Redis is the standard distributed session store for Node.js. Every instance reads from the same Redis cluster, so sessions survive restarts and horizontal scaling.

npm install express-session connect-redis
// src/middleware/session.ts
import session from 'express-session';
import { RedisStore } from 'connect-redis';
import { getRedisClient } from '../redis/client';
 
export async function createSessionMiddleware() {
  const redis = await getRedisClient();
 
  return session({
    store: new RedisStore({
      client: redis,
      prefix: 'sess:',
      ttl: 86_400, // 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, // 24 hours
      sameSite: 'lax',
    },
  });
}

Rate Limiting with Redis

Atomic INCR and EXPIRE commands make Redis ideal for distributed rate limiting.

// src/middleware/rateLimiter.ts
import { Request, Response, NextFunction } from 'express';
import { getRedisClient } from '../redis/client';
 
interface RateLimitConfig {
  windowMs: number;   // Time window in ms
  max: number;        // Max requests per window
  keyPrefix?: string;
}
 
export function createRateLimiter({ windowMs, max, keyPrefix = 'rl' }: RateLimitConfig) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const redis = await getRedisClient();
    const identifier = req.ip ?? 'unknown';
    const key = `${keyPrefix}:${identifier}`;
    const windowSecs = Math.floor(windowMs / 1000);
 
    // Atomic increment — safe for concurrent requests
    const count = await redis.incr(key);
 
    if (count === 1) {
      // First request in window — set expiry
      await redis.expire(key, windowSecs);
    }
 
    const ttl = await redis.ttl(key);
 
    res.setHeader('X-RateLimit-Limit', max);
    res.setHeader('X-RateLimit-Remaining', Math.max(0, max - count));
    res.setHeader('X-RateLimit-Reset', Date.now() + ttl * 1000);
 
    if (count > max) {
      return res.status(429).json({
        error: 'Too many requests',
        retryAfter: ttl,
      });
    }
 
    next();
  };
}
 
// Usage
app.use('/api/', createRateLimiter({ windowMs: 60_000, max: 100 }));
app.use('/api/auth/', createRateLimiter({ windowMs: 60_000, max: 10, keyPrefix: 'auth-rl' }));

Pub/Sub for Real-Time Messaging

Redis pub/sub decouples publishers from subscribers across multiple Node.js processes.

// src/pubsub/index.ts
import { createClient } from 'redis';
 
// Separate clients for pub and sub — subscribed clients cannot issue other commands
const publisher = createClient({ url: process.env.REDIS_URL });
const subscriber = publisher.duplicate();
 
await Promise.all([publisher.connect(), subscriber.connect()]);
 
// Publish an event
export async function publishEvent(channel: string, data: unknown) {
  await publisher.publish(channel, JSON.stringify(data));
}
 
// Subscribe to a channel
export async function subscribeToChannel(
  channel: string,
  handler: (data: unknown) => void
) {
  await subscriber.subscribe(channel, (message) => {
    try {
      handler(JSON.parse(message));
    } catch (err) {
      console.error(`Error processing message from ${channel}:`, err);
    }
  });
}
 
// Example: broadcast user events to all server instances
await subscribeToChannel('user:events', (event: unknown) => {
  const { type, userId } = event as { type: string; userId: string };
  if (type === 'logout') {
    // Invalidate in-memory caches across all instances
    localCache.delete(`user:${userId}`);
  }
});

Distributed Locks with SETNX

Prevent race conditions in distributed systems using Redis-based locks.

export async function withLock<T>(
  lockKey: string,
  ttlSeconds: number,
  fn: () => Promise<T>
): Promise<T | null> {
  const redis = await getRedisClient();
  const lockValue = crypto.randomUUID();
 
  // SET key value NX EX ttl — atomic lock acquisition
  const acquired = await redis.set(lockKey, lockValue, {
    NX: true,  // Only set if key does not exist
    EX: ttlSeconds,
  });
 
  if (!acquired) {
    console.log(`Could not acquire lock: ${lockKey}`);
    return null;
  }
 
  try {
    return await fn();
  } finally {
    // Only delete if we still own the lock (check value)
    const currentValue = await redis.get(lockKey);
    if (currentValue === lockValue) {
      await redis.del(lockKey);
    }
  }
}
 
// Usage
const result = await withLock('payment:user-123', 30, async () => {
  return processPayment(userId, amount);
});

Common Mistakes

Mistake 1 — Using the same Redis client for pub/sub: a subscribed Redis client is in a blocking state and cannot run other commands. Always use a separate client for subscriptions.

Mistake 2 — Not setting TTLs on cached keys: without TTL, cache keys accumulate indefinitely and Redis runs out of memory. Always use setEx or call expire.

Mistake 3 — Caching mutable data without invalidation: stale cache is worse than no cache. Build cache invalidation into every write path.

Mistake 4 — Storing large objects in Redis: Redis is not a document store. Keep values small — store IDs and reference them, or store only the fields you actually read.

Best Practices

  • Use key prefixes (user:, sess:, rl:) to namespace keys and make memory inspection easier with SCAN.
  • Set maxmemory-policy allkeys-lru in Redis config so it evicts least-recently-used keys when memory is full, acting as a proper cache.
  • Use Redis pipelines (redis.multi()) for multiple related commands to reduce round-trip latency.
  • Monitor redis.info('stats') in production — track keyspace_hits vs keyspace_misses to measure cache effectiveness.
  • Use Redis Cluster or Redis Sentinel in production for high availability — a single Redis instance is a single point of failure.

Key Takeaways

  • Redis is an in-memory data store capable of sub-millisecond reads, making it the standard caching layer for Node.js applications.
  • The cache-aside pattern is the most common: read from cache, fall back to DB on miss, then populate cache.
  • Pub/Sub requires separate Redis client instances for publisher and subscriber — subscribed clients cannot issue normal commands.
  • INCR + EXPIRE is the atomic pattern for distributed rate limiting without race conditions.
  • SET key value NX EX ttl is the standard distributed lock pattern — check the lock value before releasing to prevent stealing.
  • Always set TTLs on cached keys to prevent unbounded memory growth.
  • Use maxmemory-policy allkeys-lru to configure Redis as a proper LRU cache that evicts old keys automatically.
  • In multi-instance Node.js deployments, Redis sessions ensure every instance reads from the same session state.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading