Back to Blog
Backend Architecture Redis Caching Performance

Redis Caching: Speed Up APIs and Slash Cloud Bills

• 11 min read • Written by Marlon

TL;DR:

  • The Trap: Naive redis.get() lookups with rigid Time-To-Live (TTL) values trigger catastrophic “Cache Stampedes”—where an expiring key under 2,000 req/s causes thousands of simultaneous database queries that crash PostgreSQL connection pools.
  • The Architecture: A resilient 4-tier caching engine combining L1 in-memory LRU buffers (<1ms), distributed Redis L2 caches (<3ms), XFetch probabilistic early recomputation to preempt stampedes, and tag-based invalidation without blocking KEYS * sweeps.
  • The Business Impact: Absorbs over 85% of relational database read traffic, reduces API p99 response times from 350ms down to 4ms, and slashes cloud database provisioning costs by up to 70%.

The Read Query Bleed: Why Your Database Suffers Unnecessarily

Querying your relational database fifty times every second for the exact same catalog records, user permissions, or pricing matrices that have not changed since yesterday is lighting compute money on fire.

As your SaaS application attracts genuine enterprise adoption, customer-facing response times begin deteriorating. PostgreSQL CPU utilization creeps dangerously toward 90%, and read locks cause connection spikes that stall critical write operations.

The instinct of inexperienced teams is upgrading to an expensive Amazon RDS or Google Cloud SQL instance, writing a four-figure monthly check to cloud providers to mask architectural debt.

Yet repeatedly recalculating static data on physical disk or NVMe drives is financial waste when it can be served in under two milliseconds directly from memory RAM.

Implementing an enterprise-grade in-memory caching tier with Redis acts as an architectural shock absorber that shields your primary database, guarantees sub-10ms response times, and cuts compute bills dramatically.

  1. STAGE 01 <0.5ms

    L1 In-Memory LRU

    Local process cache absorbs hyper-frequent hot keys without traversing the local network stack.

  2. STAGE 02 <2ms

    L2 Distributed Redis

    Centralized in-memory store shared across all cluster replicas, returning structured binary buffers.

  3. STAGE 03 Preemptive

    XFetch Recomputation

    Probabilistic algorithm triggers background refresh before TTL expiration, preventing stampedes.

  4. STAGE 04 Single-Flight

    Atomic Lock & DB Set

    Acquires distributed lock; exactly one worker recomputes from PostgreSQL and writes updated TTL.


Architectural Deep Dive: Defeating Cache Stampedes and Memory Leaks

1. The Cache Stampede (Thundering Herd) Disaster

In naive implementations of the Cache-Aside pattern:

  1. A key representing a popular resource (catalog:featured) has a 10-minute TTL.
  2. The endpoint receives 2,500 requests per second.
  3. At minute 10:00, the key expires.
  4. In the next 100 milliseconds, 250 concurrent requests simultaneously experience a Cache Miss.
  5. All 250 requests execute identical, complex SQL queries with multiple JOIN clauses against PostgreSQL.
  6. The database CPU spikes to 100%, query latency jumps from 15ms to 12,000ms, and incoming HTTP requests time out across the entire cluster.

To neutralize this, production architectures employ Probabilistic Early Expiration (The XFetch Algorithm):

$$\Delta \cdot \beta \cdot (-\ln(\text{random}())) > \text{remaining_ttl}$$

Where:

  • $\Delta$ is the computational duration required to compute the value from the database.
  • $\beta$ is an aggressiveness multiplier (typically $\ge 1.0$).
  • $\text{random}()$ generates a uniform float between $0$ and $1$.

As the key nears expiration, incoming requests probabilistically trigger a background recomputation. The closer the key is to expiring, the higher the probability that a client asynchronously refreshes it. Exactly one client updates the cache before it ever hits zero, meaning the key never actually expires under steady traffic.

2. Eviction Policies: Preventing Out-Of-Memory Crashes

Redis stores everything in RAM. If your application caches unbounded keys without an explicit eviction strategy, the Redis process will eventually consume all host memory, causing either the Linux OOM killer to terminate it or Redis to reject writes with an OOM command not allowed error.

In redis.conf, never leave the default maxmemory-policy noeviction for application cache tiers. Instead, configure:

maxmemory 2gb
maxmemory-policy allkeys-lru

allkeys-lru automatically evicts the least recently used keys when memory fills, ensuring your cache degrades gracefully as an LRU working set rather than crashing your production API.

3. Safe Invalidation: Cache Tagging vs the Destructive KEYS * Command

When an administrator updates a product in a dashboard, the cache must be invalidated.

Junior engineers frequently execute:

// NEVER DO THIS IN PRODUCTION:
const keys = await redis.keys("catalog:*");
await redis.del(keys);

Because Redis is single-threaded, KEYS * scans every single key in the database sequentially. In a database with 2,000,000 keys, KEYS * blocks all read and write commands for multiple seconds, completely freezing your application.

The Production Pattern: Tagged Invalidation Sets

  • Maintain a Redis Set for each domain tag (tag:products:catalog).
  • When caching an item, add its specific key to the tag set via SADD.
  • To invalidate the entire catalog, read members via SMEMBERS tag:products:catalog (or iterate via non-blocking SSCAN), delete only those specific keys with UNLINK, and flush the tag set.

Code Comparison: Naive Cache-Aside vs XFetch Production Engine

Naive: Unprotected Cache-Aside Vulnerable to Stampedes

// Naive Caching: Rigid TTL, vulnerable to stampedes, dangerous KEYS * invalidation
import { Redis } from "ioredis";

const redis = new Redis();

interface Product {
  id: string;
  name: string;
  price: number;
}

export async function getFeaturedProductsNaive(db: any): Promise<Product[]> {
  const cacheKey = "catalog:featured:products";

  // FLAW 1: Hard miss triggers immediate DB hit for 100% of concurrent callers
  const cachedData = await redis.get(cacheKey);
  if (cachedData) {
    return JSON.parse(cachedData);
  }

  // FLAW 2: Under 2,000 req/s, thousands of queries hit the DB simultaneously here
  const products = await db.query("SELECT * FROM products WHERE featured = true");

  // FLAW 3: Rigid TTL without jitter or stampede mitigation
  await redis.set(cacheKey, JSON.stringify(products), "EX", 600);

  return products;
}

export async function invalidateCatalogNaive() {
  // FLAW 4: Completely blocks Redis event loop on large datasets!
  const keys = await redis.keys("catalog:*");
  if (keys.length > 0) {
    await redis.del(...keys);
  }
}

Production: Resilient Caching Engine with XFetch & Distributed Locks

// Production Engine: XFetch Probabilistic Refresh, Single-Flight Locks & Safe Tag Invalidation
import { Redis } from "ioredis";
import { z } from "zod";
import crypto from "crypto";

const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");

const ProductSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  price: z.number().positive(),
  updated_at: z.string()
});
type Product = z.infer<typeof ProductSchema>;

interface CacheEnvelope<T> {
  value: T;
  deltaMs: number;     // Time taken to calculate/fetch from DB
  expiresAt: number;   // Epoch timestamp in milliseconds
}

export class ResilientCache {
  /**
   * Reads from Redis using the XFetch probabilistic early recomputation algorithm.
   * Preempts cache stampedes by recomputing before expiration.
   */
  static async getOrCompute<T>(
    key: string,
    ttlSeconds: number,
    computeFn: () => Promise<T>,
    options: { beta?: number; tag?: string } = {}
  ): Promise<T> {
    const beta = options.beta ?? 1.0;
    const now = Date.now();

    // 1. Fetch existing cache envelope
    const rawData = await redis.get(key);
    let envelope: CacheEnvelope<T> | null = null;

    if (rawData) {
      try {
        envelope = JSON.parse(rawData);
      } catch {
        envelope = null;
      }
    }

    // 2. Evaluate XFetch Condition: delta * beta * -ln(rand()) > remaining_ttl
    let shouldRecompute = false;

    if (!envelope) {
      shouldRecompute = true;
    } else {
      const remainingTtlMs = envelope.expiresAt - now;
      const xfetchThreshold = envelope.deltaMs * beta * -Math.log(Math.random());

      // If remaining time is smaller than stochastic threshold, recompute early
      if (remainingTtlMs <= xfetchThreshold) {
        shouldRecompute = true;
      }
    }

    // 3. Cache Hit without needed recomputation
    if (envelope && !shouldRecompute) {
      return envelope.value;
    }

    // 4. Single-Flight Concurrency Guard (Distributed Lock via SET NX)
    const lockKey = `lock:${key}`;
    const lockToken = crypto.randomUUID();
    const acquiredLock = await redis.set(lockKey, lockToken, "PX", 4000, "NX");

    // If another worker is already recomputing, return stale data if available
    if (!acquiredLock) {
      if (envelope) {
        return envelope.value; // Return stale value gracefully rather than stalling client
      }
      // If complete cold miss, wait briefly and retry
      await new Promise((resolve) => setTimeout(resolve, 60));
      return this.getOrCompute(key, ttlSeconds, computeFn, options);
    }

    // 5. Execute Database Recomputation
    const startTime = Date.now();
    try {
      const freshValue = await computeFn();
      const deltaMs = Date.now() - startTime;

      const newEnvelope: CacheEnvelope<T> = {
        value: freshValue,
        deltaMs,
        expiresAt: Date.now() + ttlSeconds * 1000
      };

      // Store in Redis with TTL plus safety padding (TTL * 1.5)
      await redis.set(key, JSON.stringify(newEnvelope), "EX", Math.ceil(ttlSeconds * 1.5));

      // Register key under Tag Set for safe grouped invalidation
      if (options.tag) {
        await redis.sadd(`tag:${options.tag}`, key);
      }

      return freshValue;
    } finally {
      // Safe lock release via Lua script (ensures we only release our own lock)
      const releaseLua = `
        if redis.call("get", KEYS[1]) == ARGV[1] then
          return redis.call("del", KEYS[1])
        else
          return 0
        end
      `;
      await redis.eval(releaseLua, 1, lockKey, lockToken);
    }
  }

  /**
   * Safely invalidates all keys under a given tag without blocking Redis via KEYS *.
   */
  static async invalidateTag(tag: string): Promise<number> {
    const tagKey = `tag:${tag}`;
    const keysToEvict = await redis.smembers(tagKey);

    if (keysToEvict.length === 0) {
      return 0;
    }

    // UNLINK performs asynchronous memory reclamation without blocking threads
    await redis.unlink(...keysToEvict);
    await redis.del(tagKey);
    return keysToEvict.length;
  }
}

Real-World Case Studies: Caching Under High Concurrency

PróximaBolilla (proximabolilla.com): Absorbing 250,000 Requests/Min

PróximaBolilla provides real-time predictive probability computations and live lottery distribution updates across Latin America and Spain.

  • The Problem: During peak evening draw intervals, traffic spiked to over 250,000 queries per minute asking for historical frequency tables and combinations. Naive queries were locking PostgreSQL tables and causing connection exhaustion.
  • The Solution: We implemented a two-tier caching topology: a tight 3-second in-process LRU cache combined with a distributed Redis L2 layer running XFetch probabilistic recomputation. Historical combinations are stored under tagged sets.
  • The Result: PostgreSQL CPU utilization dropped from 94% down to under 12%, while API p99 response times plummeted from 340ms to 3.8ms, supporting massive viral spikes without provisioning expensive database replicas.

Strategio (Startup Perú 2G Winner): Scalable Analytics at Sub-$50/Month

During the architecture and development of Strategio, the AI-powered Business Intelligence platform built from scratch in 12 weeks:

  • The Problem: Thousands of e-commerce business owners needed to query daily revenue dashboards, gross margins, and inventory velocities. Recomputing SQL aggregations on every page load would have required expensive enterprise multi-node database clusters.
  • The Solution: We implemented the Cache-Aside pattern with domain-driven tag invalidations. When new point-of-sale transactions sync, background jobs purge only the affected tenant’s tagged cache keys using UNLINK.
  • The Result: The caching architecture absorbed over 92% of all analytical read queries, allowing the entire production infrastructure to run smoothly on budget VPS nodes for under $50 USD/month.

Pre-Flight Checklist for Production API Caching

Before deploying an in-memory caching layer to production, verify these five architectural gates:

  • Stampede Defense Active: Does your caching layer use XFetch probabilistic recomputation or single-flight distributed locks (SET NX) on hot keys?
  • Explicit Eviction Policy: Is Redis configured with maxmemory-policy allkeys-lru rather than the default crash-prone noeviction?
  • Zero KEYS * Usage: Are key invalidations organized via Redis Sets (Cache Tags) and evicted using non-blocking UNLINK instead of blocking KEYS *?
  • Safe Lock Release via Lua: Do your distributed locks verify token ownership with an atomic Lua script before deleting the lock key?
  • Type Validation on Deserialization: Does your backend parse cached JSON strings through a strict schema validator (Zod or Pydantic) to detect corrupt or outdated payloads?

Architectural Advisory

Designing high-throughput, low-latency caching architectures requires deep understanding of memory footprints, concurrency primitives, and invalidation semantics.

At Coins5, Marlon helps venture-backed startups and growing platforms eliminate database bottlenecks, implement sub-millisecond caching tiers, and reduce cloud infrastructure expenses.

If your database CPU is redlining or your API latency is hurting user experience, schedule a 15-minute technical discovery call or connect directly via WhatsApp.

Crushing Database Load with High-Velocity Caching?

Let's design a resilient multi-tier caching architecture in a concise 15-minute technical discovery call.

Marlon - Senior Architect
Author & Systems Architect

Marlon / Coins5

Senior Full Stack Developer & Cloud Architect. Building custom software and production MVPs with Clean Architecture, high-concurrency cloud design, and AI velocity.

Book 15m Call