Skip to main content

On This Page

Production Node.js Caching: Implementing Redis, LRU, and CDN Edge Layers

3 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

Node.js Caching in Production: Redis, In-Memory, and CDN Edge

High-leverage caching strategies in Node.js can reduce database load by 80% and transform p99 latency from seconds to milliseconds. The AXIOM Agent highlights that while effective, poor implementation leads to thundering herds and complex cache invalidation bugs.

Why This Matters

In ideal models, caching is often viewed as a simple key-value store, but technical reality involves managing distributed state across multiple layers where network round-trips and event loop blocking become significant bottlenecks. Failing to account for distributed systems coordination can lead to split-brain scenarios and catastrophic database failures during traffic spikes.

Misconfiguring Redis can lead to O(N) blocking operations via the KEYS command, which freezes the Redis event loop. Real-world scaling requires moving beyond basic TTL-only invalidation to more robust patterns like surrogate keys and probabilistic recomputation to ensure system stability under high load.

Key Insights

  • Redis is the standard distributed cache for Node.js production systems, but developers must avoid O(N) operations like redis.keys(); instead, use the SCAN command with COUNT to prevent blocking the event loop.
  • In-memory LRU caching via libraries like lru-cache eliminates network round-trips entirely (< 1ms) and is ideal for per-process data such as JWT signing keys and parsed configuration.
  • The stale-while-revalidate CDN directive allows serving stale responses immediately while asynchronously fetching fresh copies, effectively preventing thundering herds on public API endpoints.
  • Cache Stampedes can be mitigated through the XFetch algorithm, which uses probabilistic early expiry based on recomputation costs, or via ‘Single-Flight’ promise deduplication to ensure only one upstream call occurs per cache miss.
  • A three-layer cache architecture (L1: Memory, L2: Redis, L3: Database) ensures that each layer is an order of magnitude faster than the next, with L1 refreshing from L2 to maintain consistency.

Working Examples

Standard Cache-Aside pattern (Lazy Loading) using ioredis.

const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

async function getUserById(userId) {
  const cacheKey = `user:${userId}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await db.users.findById(userId);
  if (!user) return null;

  await redis.setex(cacheKey, 300, JSON.stringify(user));
  return user;
}

Safe cache invalidation using SCAN to avoid blocking the Redis event loop.

async function deleteUserKeys(userId) {
  const pattern = `user:${userId}*`;
  let cursor = '0';
  do {
    const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
    cursor = nextCursor;
    if (keys.length > 0) await redis.del(...keys);
  } while (cursor !== '0');
}

Promise deduplication (Single-Flight) to prevent multiple concurrent upstream requests for the same key.

const inflight = new Map();

async function singleFlight(key, fetchFn) {
  if (inflight.has(key)) return inflight.get(key);
  const promise = fetchFn().finally(() => inflight.delete(key));
  inflight.set(key, promise);
  return promise;
}

Practical Applications

  • Use Case: High-traffic product inventory systems utilizing the Write-Through pattern to keep Redis consistent with the source of truth. Pitfall: Increased write latency since every update must commit to both the database and cache simultaneously.
  • Use Case: Public API caching at the edge using Cloudflare Surrogate Keys (Cache-Tags) for selective invalidation. Pitfall: Relying solely on TTL-only invalidation, which can serve stale data longer than necessary or cause mass invalidation traffic spikes.
  • Use Case: Implementing a two-layer cache (L1 LRU + L2 Redis) for feature flags and configurations. Pitfall: Setting L1 TTL longer than L2 TTL, which prevents local processes from receiving updates propagated to the distributed cache.

References:

Continue reading

Next article

REST vs GraphQL vs WebSockets vs Webhooks: A Technical Decision Guide

Related Content