Back to Blog
data9 min read

Redis Caching Patterns for Node.js Applications

Production caching — cache-aside, write-through, session management, rate limiting with sorted sets, pub/sub, and invalidation strategies.

V
By Ventra Rocket
·Published on 5 January 2026
#Redis#Caching#Node.js#Performance#Backend

Redis is the go-to caching layer for Node.js. Beyond key-value storage, it supports sorted sets, pub/sub, and atomic operations.

Setup

import { createClient } from "redis";

const redis = createClient({
  url: process.env.REDIS_URL,
  socket: { reconnectStrategy: (n) => Math.min(n * 100, 5000) },
});
redis.on("error", (err) => console.error("Redis:", err));
await redis.connect();

1. Cache-Aside Pattern

class ProductService {
  private readonly TTL = 3600;

  async getProduct(id: string): Promise<Product> {
    const key = `product:${id}`;
    const cached = await redis.get(key);
    if (cached) return JSON.parse(cached);

    const product = await db.products.findUnique({ where: { id } });
    if (!product) throw new Error("Not found");

    await redis.setEx(key, this.TTL, JSON.stringify(product));
    return product;
  }

  async updateProduct(id: string, data: Partial<Product>): Promise<Product> {
    const product = await db.products.update({ where: { id }, data });
    await redis.del(`product:${id}`);
    return product;
  }
}

2. Cache Stampede Prevention

async function getWithLock<T>(key: string, ttl: number, fn: () => Promise<T>): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const lockKey = `lock:${key}`;
  const acquired = await redis.set(lockKey, "1", { EX: 10, NX: true });

  if (acquired) {
    try {
      const data = await fn();
      await redis.setEx(key, ttl, JSON.stringify(data));
      return data;
    } finally {
      await redis.del(lockKey);
    }
  }
  await new Promise((r) => setTimeout(r, 100));
  return getWithLock(key, ttl, fn);
}

3. Session Management

const TTL = 86400;

async function createSession(userId: string): Promise<string> {
  const id = crypto.randomUUID();
  await redis.setEx(`session:${id}`, TTL, JSON.stringify({ userId, createdAt: Date.now() }));
  return id;
}

async function getSession(id: string) {
  const raw = await redis.get(`session:${id}`);
  if (!raw) return null;
  await redis.expire(`session:${id}`, TTL);  // Sliding expiration
  return JSON.parse(raw);
}

4. Rate Limiting with Sorted Sets

async function checkRateLimit(
  id: string,
  limit: number,
  windowSec: number
): Promise<{ allowed: boolean; remaining: number }> {
  const key = `ratelimit:${id}`;
  const now = Date.now();
  const windowStart = now - windowSec * 1000;

  const pipeline = redis.multi();
  pipeline.zRemRangeByScore(key, "-inf", windowStart);
  pipeline.zAdd(key, { score: now, value: `${now}` });
  pipeline.zCard(key);
  pipeline.expire(key, windowSec);

  const results = await pipeline.exec();
  const count = results[2] as number;
  return { allowed: count <= limit, remaining: Math.max(0, limit - count) };
}

5. Pub/Sub

const pub = redis.duplicate();
const sub = redis.duplicate();
await Promise.all([pub.connect(), sub.connect()]);

// Publish event
await pub.publish("inventory:updated", JSON.stringify({ productId: "123", qty: 50 }));

// Subscribe
await sub.subscribe("inventory:updated", (msg) => {
  const { productId, qty } = JSON.parse(msg);
  // Broadcast to WebSocket clients
});

Anti-Patterns

| Anti-Pattern | Problem | Fix | |-------------|---------|-----| | No TTL | Memory leak | Always set TTL | | Caching everything | Stale data, memory waste | Cache expensive reads | | Values > 100KB | Slow ops | Store references | | One key for all users | Invalidation storms | Per-resource keys |

Conclusion

Redis is most powerful when used for the right operations: cache-aside for DB offloading, sorted sets for rate limiting, atomic operations for distributed locks. Always measure hit rates and plan invalidation before writing cache code.

Related Articles

Redis Caching Patterns for Node.js Applications | Ventra Rocket