Skip to content
advancedPhase 46 · Caching

Cache Stampede

Prevent thundering herd when many requests hit expired cache simultaneously.

45m
0 problems
Topic Progress0%

Thundering Herd

Thundering Herd Problem

Cache stampede (thundering herd) occurs when many concurrent requests try to populate the same cache entry simultaneously.

The Problem

Cache Stampede Scenario:

1. Popular key expires
2. 1000 concurrent requests check cache
3. All see cache miss
4. All query database simultaneously
5. Database overwhelmed, slow response
6. All 1000 responses write to cache

Timeline:
Request 1: Cache miss → DB query ────────────────→ Cache set
Request 2: Cache miss → DB query ────────────────→ Cache set
Request 3: Cache miss → DB query ────────────────→ Cache set
...
Request 1000: Cache miss → DB query ─────────────→ Cache set

Result: 1000 duplicate DB queries!

Real-World Example

# Problematic code
def get_popular_product(product_id):
    cache_key = f"product:{product_id}"
    
    # Race condition here!
    product = cache.get(cache_key)
    if product is None:
        # Multiple threads enter here simultaneously
        product = db.query_product(product_id)  # 1000x DB queries!
        cache.set(cache_key, product, ttl=3600)
    
    return product

Impact

Metric Normal During Stampede
DB Load 100 QPS 10,000+ QPS
Response Time 10ms 5000ms+
Error Rate 0.1% 50%+
Cache Hit Ratio 95% 0% (temporarily)

Prevention Strategies

Prevention Strategies

1. Mutex/Lock Pattern

import threading
import time

class CacheStampedePrevention:
    def __init__(self, cache_client):
        self.cache = cache_client
        self.locks = {}
        self.lock_mutex = threading.Lock()
    
    def get_with_lock(self, key, loader, ttl=3600, lock_timeout=5):
        """Prevent stampede with distributed lock"""
        # Try cache first
        value = self.cache.get(key)
        if value is not None:
            return value
        
        # Acquire lock
        lock_key = f"lock:{key}"
        if self._acquire_lock(lock_key, lock_timeout):
            try:
                # Double-check after acquiring lock
                value = self.cache.get(key)
                if value is not None:
                    return value
                
                # Load and cache
                value = loader()
                self.cache.set(key, value, ttl)
                return value
            finally:
                self._release_lock(lock_key)
        else:
            # Another thread is loading, wait and retry
            time.sleep(0.1)
            return self.cache.get(key)
    
    def _acquire_lock(self, lock_key, timeout):
        deadline = time.time() + timeout
        while time.time() < deadline:
            if self.cache.set(lock_key, '1', nx=True, ex=timeout):
                return True
            time.sleep(0.01)
        return False
    
    def _release_lock(self, lock_key):
        self.cache.delete(lock_key)

2. Early Expiration

def get_with_early_expiration(key, loader, ttl=3600, early_pct=0.1):
    """Refresh cache before it expires"""
    data = cache.get(key)
    
    if data:
        data = json.loads(data)
        age = time.time() - data['created_at']
        
        # If close to expiration, refresh in background
        if age > ttl * (1 - early_pct):
            background_refresh(key, loader, ttl)
        
        return data['value']
    
    # No data, must load synchronously
    value = loader()
    cache.setex(key, ttl, json.dumps({
        'value': value,
        'created_at': time.time()
    }))
    return value

3. Probabilistic Early Expiration

import random

def get_probabilistic(key, loader, ttl=3600, beta=1.0):
    """XFetch algorithm - probabilistic refresh"""
    data = cache.get(key)
    
    if data:
        data = json.loads(data)
        delta = time.time() - data['created_at']
        
        # Calculate refresh probability
        # Higher delta = higher chance of refresh
        p = (beta * delta) / ttl
        
        if random.random() < p:
            # Probabilistically refresh
            try:
                value = loader()
                cache.setex(key, ttl, json.dumps({
                    'value': value,
                    'created_at': time.time()
                }))
                return value
            except:
                pass  # Use cached value on failure
        
        return data['value']
    
    # No data, load synchronously
    value = loader()
    cache.setex(key, ttl, json.dumps({
        'value': value,
        'created_at': time.time()
    }))
    return value

Locking Patterns

Locking Patterns

Distributed Lock with Redis

class DistributedLock:
    def __init__(self, redis_client):
        self.redis = redis_client
    
    def acquire(self, key, timeout=10, retry_count=3, retry_delay=0.1):
        """Acquire distributed lock with retry"""
        for i in range(retry_count):
            if self.redis.set(key, '1', nx=True, ex=timeout):
                return True
            time.sleep(retry_delay)
        return False
    
    def release(self, key):
        """Release distributed lock"""
        self.redis.delete(key)

# Usage
lock = DistributedLock(redis)
lock_key = f"lock:product:{product_id}"

if lock.acquire(lock_key):
    try:
        # Only one process loads data
        product = db.query(product_id)
        cache.set(f"product:{product_id}", product)
    finally:
        lock.release(lock_key)

Read-Write Lock Pattern

class ReadWriteLock:
    def __init__(self, redis_client):
        self.redis = redis_client
    
    def acquire_read(self, key):
        read_key = f"read:{key}"
        return self.redis.incr(read_key) == 1
    
    def release_read(self, key):
        self.redis.decr(f"read:{key}")
    
    def acquire_write(self, key, timeout=5):
        write_key = f"write:{key}"
        return self.redis.set(write_key, '1', nx=True, ex=timeout)
    
    def release_write(self, key):
        self.redis.delete(f"write:{key}")

# Multiple readers, single writer
def get_with_read_write_lock(key, loader):
    rw_lock = ReadWriteLock(redis)
    
    # Try read lock first
    if rw_lock.acquire_read(key):
        try:
            value = cache.get(key)
            if value:
                return value
        finally:
            rw_lock.release_read(key)
    
    # Acquire write lock
    if rw_lock.acquire_write(key):
        try:
            # Double-check
            value = cache.get(key)
            if value:
                return value
            
            value = loader()
            cache.set(key, value)
            return value
        finally:
            rw_lock.release_write(key)

Best Practices

  1. Always use timeouts on locks to prevent deadlocks
  2. Implement retry with backoff for lock acquisition
  3. Use unique lock values for safe release
  4. Consider lock-free approaches when possible
  5. Monitor lock contention metrics

Practice Problems

0/3solved
Design Cache Stampede System

Design a scalable Cache Stampede system. Cover high-level architecture, data model, and API design.

Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliability
Cache Stampede Scaling

How would you scale Cache Stampede to handle 10x the current load? Identify bottlenecks and solutions.

Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decomposition
Cache Stampede Failure Modes

Analyze potential failure modes for Cache Stampede and design mitigation strategies.

Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradation

Quiz

1. What causes a cache stampede?

Question 1 options

2. How does a mutex prevent cache stampede?

Question 2 options

3. What is early expiration in cache stampede prevention?

Question 3 options

4. Why use double-check locking?

Question 4 options

5. What happens if a distributed lock is not released?

Question 5 options

Flashcards

Question

What is cache stampede (thundering herd)?

Answer

When many concurrent requests try to populate the same cache entry simultaneously after it expires, overwhelming the database

Question

How to prevent cache stampede with locks?

Answer

Use distributed lock so only one request loads from DB, others wait and retry cache read

Question

What is early expiration?

Answer

Refreshing cache before TTL expires (e.g., at 90%) to prevent the stampede window

Question

Why double-check after acquiring lock?

Answer

Another thread may have already populated the cache while waiting for the lock, avoiding redundant DB queries

Question

What is probabilistic early expiration?

Answer

XFetch algorithm - probabilistically refreshes cache based on age, spreading load over time

Revision Notes

Key Takeaways

  • 1.Cache stampede happens when expired popular keys get many concurrent requests
  • 2.Use distributed locks to ensure only one request loads from DB
  • 3.Double-check cache after acquiring lock to avoid redundant queries
  • 4.Early expiration prevents stampede by refreshing before TTL
  • 5.Always use lock timeouts to prevent deadlocks

Interview Tips

  • Describe the thundering herd problem clearly
  • Explain the mutex pattern with double-check
  • Discuss early expiration and probabilistic approaches
  • Mention monitoring lock contention in production

Cheat Sheet

Cheat Sheet: Cache Stampede

Problem

  • Popular key expires
  • Many concurrent requests
  • All hit DB simultaneously
  • Database overwhelmed

Prevention

  1. Mutex/Lock: One request loads, others wait
  2. Early Expiration: Refresh before TTL
  3. Probabilistic (XFetch): Random refresh

Lock Pattern

  1. Try cache
  2. Acquire lock (with timeout)
  3. Double-check cache
  4. Load from DB
  5. Set cache
  6. Release lock

Best Practices

  • Use timeouts on locks
  • Retry with backoff
  • Monitor lock contention