NITAI IT SOLUTION

Redis Caching Patterns for High-Traffic Applications: Cache-Aside, Write-Through, and Rate Limiting

Master advanced Redis in-memory caching architectures: Cache stampede prevention, sliding-window rate limiting, distributed locking, and cache invalidation.

As web traffic grows from thousands to millions of requests per minute, querying relational disk databases for every incoming HTTP request becomes unsustainable. Disk I/O saturation, connection contention, and slow serialization lead to soaring infrastructure bills and dropped connections. Operating as an ultra-fast in-memory key-value data store, Redis handles over 100,000 operations per second with sub-millisecond latency. This guide explores the foundational caching patterns required for bulletproof scale.

Core Redis Caching Architectural Patterns

Choosing the right caching pattern depends on your application's read-to-write ratio and tolerance for eventual consistency:

Caching Pattern How It Works Optimal Use Case
Cache-Aside (Lazy Loading) App queries cache first. On cache miss, queries DB, populates cache, returns data. Heavy read-dominant workloads (e.g., product catalogs, public profiles, blog posts)
Write-Through Cache App writes data simultaneously to both the cache and the primary DB in one atomic transaction. Critical data that must immediately reflect updates without stale reads (e.g., user profiles)
Write-Behind (Write-Back) App writes data instantly to Redis cache; asynchronous worker flushes updates to DB in batches. Ultra-high-write workloads (e.g., live streaming viewer counts, click tracking, telemetry)

Preventing the "Cache Stampede" (Dogpiling Effect)

What happens when a cached item visited by 10,000 concurrent users suddenly expires at the exact same millisecond? All 10,000 requests experience a cache miss simultaneously and hammer the primary database at the same time, instantly crashing the database server.

The Solution: Probabilistic Early Expiration (XFetch Algorithm) or Distributed Mutex Locks:

// services/cacheService.ts
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);

export async function getWithStampedeProtection(key: string, ttlSeconds: number, fetchFromDb: () => Promise<any>) {
  const cached = await redis.get(key);
  if (cached) {
    return JSON.parse(cached);
  }

  // Acquire a distributed lock for 5 seconds to ensure ONLY 1 worker hits the DB
  const lockAcquired = await redis.set(`lock:${key}`, '1', 'EX', 5, 'NX');

  if (lockAcquired) {
    try {
      console.log('🔒 Lock acquired. Fetching fresh data from primary DB...');
      const freshData = await fetchFromDb();
      // Add random jitter to TTL (+/- 10%) so keys never expire at identical second
      const jitteredTtl = ttlSeconds + Math.floor(Math.random() * 60);
      await redis.set(key, JSON.stringify(freshData), 'EX', jitteredTtl);
      return freshData;
    } finally {
      await redis.del(`lock:${key}`);
    }
  } else {
    // Other workers sleep 100ms and re-check cache instead of slamming DB
    await new Promise((r) => setTimeout(r, 100));
    return getWithStampedeProtection(key, ttlSeconds, fetchFromDb);
  }
}

Sliding-Window Rate Limiting via Redis Sorted Sets (ZSET)

Protecting public API endpoints from DDoS attacks, brute-force login attempts, and scraper bots requires accurate rate limiting. Simple fixed-window counters allow 2x traffic bursts at window boundaries. The **Sliding Window Log** pattern using Redis Sorted Sets provides 100% mathematical accuracy:

// middleware/rateLimiter.ts
export async function isRateLimited(userId: string, limit = 60, windowSeconds = 60): Promise<boolean> {
  const key = `ratelimit:${userId}`;
  const now = Date.now();
  const windowStart = now - (windowSeconds * 1000);

  const multi = redis.multi();
  // 1. Remove timestamps older than the active sliding window
  multi.zremrangebyscore(key, 0, windowStart);
  // 2. Add current request timestamp
  multi.zadd(key, now, `${now}`);
  // 3. Count total requests in window
  multi.zcard(key);
  // 4. Set expiry on key
  multi.expire(key, windowSeconds);

  const results = await multi.exec();
  const requestCount = results![2][1] as number;

  return requestCount > limit;
}

Frequently Asked Questions

What Redis eviction policy is best for caching?

For general application caching, configure maxmemory-policy allkeys-lru (Least Recently Used) or volatile-lru. When RAM reaches capacity, Redis automatically evicts the oldest unused cache keys without throwing Out-Of-Memory errors.

Can Redis be used as a primary database?

While Redis supports data persistence to disk via RDB snapshots and AOF (Append-Only File) logs, it is fundamentally an in-memory data store. Best practice is to use PostgreSQL or MySQL as the durable system of record and Redis as the ephemeral high-speed caching and queue layer.

How does Redis Pub/Sub differ from Kafka or RabbitMQ?

Redis Pub/Sub is an ultra-fast, "fire-and-forget" messaging mechanism with zero message persistence. If a subscriber is offline when a message is published, it is lost forever. For mission-critical background jobs requiring guaranteed delivery and replayability, use Redis Streams or dedicated brokers like RabbitMQ or Kafka.

Need Help Implementing This in Production?

Our senior engineering team helps startups and enterprises scale web applications, migrate cloud infrastructure, and build custom AI workflows with guaranteed delivery timelines.

Schedule Free Technical Consultation Chat with Lead Architect