Node.js cookbook · Beginner

Cache expensive lookups in Redis from Node.js

Reduce database load for a hot endpoint while avoiding stampedes when entries expire.

Approach

Implement cache-aside with JSON values and TTL jitter. Deduplicate concurrent misses inside a process with an in-flight Map of promises, and optionally across processes with a short Redis lock.

Solution

import { createClient } from "redis";

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const inflight = new Map<string, Promise<unknown>>();

export async function cached<T>(key: string, ttlSeconds: number, load: () => Promise<T>): Promise<T> {
  const hit = await redis.get(key);
  if (hit !== null) return JSON.parse(hit) as T;

  const pending = inflight.get(key) as Promise<T> | undefined;
  if (pending) return pending;                       // coalesce concurrent misses in this process

  const promise = (async () => {
    try {
      const value = await load();
      const jitter = Math.floor(Math.random() * ttlSeconds * 0.1);
      await redis.set(key, JSON.stringify(value), { EX: ttlSeconds + jitter });
      return value;
    } finally {
      inflight.delete(key);
    }
  })();
  inflight.set(key, promise);
  return promise;
}

app.get("/api/products/:id", async (req, res) => {
  const product = await cached(`product:v1:${req.params.id}`, 300, () => products.get(req.params.id));
  if (!product) return res.status(404).json({ title: "Not Found" });
  res.json(product);
});

export const invalidateProduct = (id: string) => redis.del(`product:v1:${id}`);

Pitfalls

  • Caching per-user responses under shared keys.
  • Not versioning keys when the value shape changes.
  • Treating the cache as the source of truth.

Variations

  • In-process LRU (lru-cache) in front of Redis for ultra-hot keys.
  • HTTP caching headers + CDN for public data.

Further reading: Redis: Node.js client

Reference: https://redis.io/docs/latest/develop/clients/nodejs/

More Node.js recipes