Node.js performance engineering

Redis Cache-Aside in Node.js: A Production Guide

A cache is a disposable performance layer, not a second source of truth. Design staleness, failure, and invalidation explicitly so Redis makes the application faster without making its data less trustworthy.

Resilient cache layer routing application requests between Node.js services and a durable database

Cache-aside keeps the database authoritative

In cache-aside, the application first asks Redis for an entry. A hit returns immediately. A miss reads the durable database and stores the result with an expiry before returning it. Writes go to the database first, then delete the affected cache entry. The next read repopulates it.

This pattern fits read-heavy product catalogues, public profiles, configuration, computed summaries, and API responses where bounded staleness is acceptable. Redis's official cache-aside guide recommends per-key expiry and explicit invalidation on writes. It also distinguishes this model from write-through or write-behind systems that make the cache part of the write path.

1. ReadNode.js checks Redis
2. MissLoad from the database
3. FillStore with a bounded TTL
4. WriteCommit DB, then delete cache

Do not cache authentication decisions, balances, inventory reservations, or other correctness-critical state merely because the data is frequently read. If a stale answer can authorize the wrong action or commit an invalid transaction, fetch authoritative data or use a design with stronger consistency.

Design keys and TTLs from the data contract

A useful key identifies the environment, schema version, entity, and identifier: prod:v3:product:42. Versioning lets a deployment change serialized shape without parsing older entries. Keep tenant identity in the key for tenant-scoped data, and never put secrets or personal data in key names because keys appear in metrics and operational tools.

Freshness budget

The TTL should reflect the oldest acceptable answer, not a universal caching default.

Explicit invalidation

Delete after a successful database write when waiting for expiry would violate the product expectation.

Jitter

Add a small random range so many related keys do not expire in the same instant.

A TTL bounds staleness; it does not guarantee freshness. If a record changes immediately after a fill, the old value can remain until invalidation or expiry. Cache a not-found sentinel briefly to absorb repeated probes for missing IDs, but use a shorter lifetime than positive entries so newly created records become visible quickly.

Build a typed cache-aside helper with safe degradation

The helper below uses the official redis client, JSON serialization, jittered expiry, negative caching, and database fallback. In real code, validate deserialized values with a runtime schema rather than trusting cached JSON.

import { createClient } from "redis";

type CacheResult<T> = { found: true; value: T } | { found: false };
const redis = createClient({ url: process.env.REDIS_URL });
redis.on("error", (error) => logger.warn({ error }, "Redis error"));

export async function cacheAside<T>(options: {
  key: string;
  ttlSeconds: number;
  load: () => Promise<T | null>;
}): Promise<T | null> {
  const { key, ttlSeconds, load } = options;
  try {
    const cached = await redis.get(key);
    if (cached) {
      const parsed = JSON.parse(cached) as CacheResult<T>;
      return parsed.found ? parsed.value : null;
    }
  } catch (error) {
    logger.warn({ error, key }, "Cache read failed; using primary store");
  }

  const value = await load();
  const jitter = Math.floor(Math.random() * Math.max(1, ttlSeconds * 0.1));
  const ttl = value === null ? Math.min(30, ttlSeconds) : ttlSeconds + jitter;
  const payload: CacheResult<T> = value === null
    ? { found: false }
    : { found: true, value };

  try {
    await redis.set(key, JSON.stringify(payload), { EX: ttl });
  } catch (error) {
    logger.warn({ error, key }, "Cache fill failed");
  }
  return value;
}

Create one shared client during process startup, attach an error listener, and confirm readiness before serving traffic. Redis documents explicit connection, connection events, TLS configuration, and reconnect strategies in its node-redis connection guide. Keep credentials in REDIS_URL or a secret manager; do not embed them in source.

The database load remains the correctness path. A Redis read or fill failure should normally degrade performance, not fail an otherwise valid request. That rule changes when Redis is intentionally authoritative for a feature such as distributed coordination; label those dependencies separately.

Commit the database before invalidating the cache

export async function updateProduct(id: string, input: ProductUpdate) {
  const product = await database.product.update({ id, input });
  try {
    await redis.del(`prod:v3:product:${id}`);
  } catch (error) {
    logger.error({ error, id }, "Cache invalidation failed");
  }
  return product;
}

Deleting before the database commit creates a race: another request can miss, read the old database value, and refill the stale entry just before the write commits. Database-first deletion narrows the risk, but it does not make two independent systems atomic. If invalidation must survive process crashes, record an outbox event in the database transaction and let a retryable worker delete or version affected keys. Our transactional outbox guide explains that delivery pattern.

Prefer deletion to rewriting after a mutation. Deletion keeps one transformation path—the normal read loader—and reduces the chance that write code and read code serialize different shapes. For list or aggregate caches, maintain an explicit dependency map or bump a version token; broad wildcard deletion is slow, difficult to reason about, and dangerous in shared Redis deployments.

Prevent hot-key expiry from becoming a database incident

When a popular key expires, hundreds of workers can observe the same miss and query the database together. TTL jitter spreads predictable expiry, but a single hot key still needs request coalescing. Start with an in-process map of pending promises so concurrent misses within one Node.js instance share a load. At higher scale, use a short distributed fill lease created with atomic SET ... NX PX; Redis documents those conditional and expiry options in the SET command reference.

  • Give the lease a unique owner token and a short, bounded lifetime.
  • Release it only if the stored token still belongs to the caller, using an atomic script or supported conditional operation.
  • If another process owns the lease, wait briefly with jitter, recheck the cache, then fall back according to the endpoint's latency budget.
  • Never hold a lease while performing unrelated work.

A lock is not the only option. For data that tolerates slightly older answers, stale-while-revalidate can serve a soft-expired entry while one worker refreshes it. Choose the policy per endpoint and test the cold-cache case during capacity planning.

Decide whether each endpoint fails open or closed

FailureRecommended responseGuardrail
Redis timeoutRead the primary databaseBound cache latency so fallback starts promptly
Cache fill failureReturn the database resultLog once at an appropriate level; avoid retry storms
Malformed entryDelete or ignore, then reloadRuntime validation and versioned keys
Database failure on missReturn an error, or serve explicitly permitted stale dataNever silently invent a successful response
Invalidation failureAlert and retry through an outboxTTL remains the final staleness bound

Redis's Node.js error-handling guide distinguishes transient network failures from programming errors such as wrong data types. Retry only bounded, recoverable operations with backoff and jitter. Unlimited retries increase latency and can synchronize a fleet against a struggling dependency.

Secure, size, and observe Redis as shared infrastructure

Use TLS, authentication and least-privilege ACLs; restrict network access; separate environments; and set memory and eviction policy deliberately. Do not expose Redis directly to the public internet. Treat cached personal or regulated data with the same access controls and retention review as its source.

Measure hit, miss, negative-hit, fill, invalidation and parse-failure counts; cache-operation latency; fallback database latency; hot keys; memory; eviction; expiry; connection and reconnect events; and database load during cold starts. A high hit rate is not automatically healthy: it may hide stale data, oversized entries, or a cache that cannot be rebuilt safely.

Run failure tests: disconnect Redis, flush a staging cache, expire a hot key under load, rotate credentials, inject malformed data, and verify that an invalidation event retries. Coordinate cache capacity with connection pooling and dependency timeouts described in our Node.js connection-pooling guide and request deadline guide.

Redis cache-aside production checklist

✓ Database remains the source of truth

✓ Every entry has a freshness-based TTL

✓ Keys include environment and schema version

✓ Writes commit before cache deletion

✓ Invalidation failures are durable and retryable

✓ TTLs include jitter

✓ Hot misses are coalesced

✓ Missing records use short negative caching

✓ Redis failure falls back within a latency budget

✓ Cached JSON is validated

✓ TLS, ACLs and network restrictions are enabled

✓ Cold-cache and outage behavior are load-tested

Choose caching only after defining correctness

The best cache design starts with a plain-language staleness promise: what may be old, for how long, and what happens when Redis disappears. From there, cache-aside is intentionally simple. The database owns truth; Redis accelerates reads; expiry bounds mistakes; deletion follows writes; and metrics prove whether the layer helps.

Endurance Softwares helps teams design and modernize Node.js backends, APIs, databases, caching, observability, and cloud infrastructure with production failure modes in view from the start.

Shares

Design a backend that stays fast under real load

We can help review caching boundaries, database pressure, failure behavior, and rollout plans for your Node.js application.

Discuss Your Backend Architecture

Get Quote
Let's build something powerful

Have a project idea? Let’s turn it into a scalable product.

Book Free Consultation

© 2026 Endurance Softwares. All rights reserved.