Menu

Cache-Aside Pattern

Load data into cache on demand: the cache-aside flow, cache miss handling, consistency considerations, and stampede prevention.

12 min readHigh interview weight

What Is Cache-Aside?

Cache-Aside (also called Lazy Loading) is the most common caching pattern used in production systems. The application code is responsible for managing the cache — data is only loaded into the cache when it is actually requested, and the application must handle both cache hits and misses explicitly. Neither the cache nor the data store knows about each other; the application sits in the middle and mediates all reads and writes.

This pattern is the default choice at companies like Netflix, Twitter, and Airbnb for their Redis-based caching layers because it gives engineers full control over what goes into the cache, when it expires, and how it is invalidated.

The Cache-Aside Read Flow

The read path follows a consistent three-step check. On every read, the application first looks in the cache. If the value is present (cache hit), it is returned immediately. If the value is absent (cache miss), the application fetches the data from the primary data store, writes the result into the cache with an appropriate TTL, then returns the value to the caller.

Loading diagram...
Cache-Aside read flow: miss path fetches from DB and populates the cache

The Cache-Aside Write Flow

On writes, the application updates the primary data store directly, then invalidates (deletes) the corresponding cache entry rather than updating it. The updated value will be re-populated on the next read. This avoids the race condition of writing a stale value to the cache while a concurrent request might be fetching fresh data.

⚠️

Invalidate, Don't Update

On writes, always delete the cache key rather than writing the new value. Updating the cache on write introduces race conditions: two concurrent writers could update the cache in the wrong order, leaving stale data permanently.

python
def get_user(user_id: str) -> User:
    cache_key = f"user:{user_id}"

    # Step 1: Try cache first
    cached = redis.get(cache_key)
    if cached:
        return deserialize(cached)

    # Step 2: Cache miss — fetch from DB
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)
    if not user:
        return None

    # Step 3: Populate cache with TTL
    redis.setex(cache_key, ttl=3600, value=serialize(user))
    return user

def update_user(user_id: str, data: dict) -> None:
    # Step 1: Write to DB first
    db.execute("UPDATE users SET ... WHERE id = ?", user_id, data)

    # Step 2: Invalidate cache (delete, not update)
    redis.delete(f"user:{user_id}")

Consistency Considerations

Cache-Aside provides eventual consistency. Between the moment a write invalidates the cache and a reader re-populates it, all readers go to the database. This is generally acceptable but has two edge cases worth knowing:

  • Stale reads after write: If invalidation fails (network blip to Redis), the cache holds stale data until TTL expiry. Always set a reasonable TTL as a safety net.
  • Cold start penalty: A freshly deployed service or a cache flush means every request hits the database. Use cache warming strategies for predictable cold-start scenarios.
  • Cache stampede (thundering herd): When a popular key expires, many concurrent requests all miss and simultaneously query the database, causing a spike.

Cache Stampede Prevention

When a high-traffic cache key expires, hundreds of simultaneous requests can hammer the database at once — the cache stampede or thundering herd problem. Three standard mitigations exist:

TechniqueHow It WorksTrade-off
Mutex / LockFirst miss acquires a distributed lock and re-populates; others waitAdded latency for waiters; lock must be released on failure
Probabilistic Early ExpiryRe-compute value slightly before TTL expires with some probabilitySlight over-computation but no lock contention
Background RefreshServe stale data immediately; async worker refreshes in backgroundRequires a separate TTL for stale-ok window
💡

Interview Tip

Interviewers love asking about cache stampede. Lead with the mutex lock approach, then mention probabilistic early expiry (also called 'XFetch') as a lock-free alternative. Demonstrating knowledge of both signals depth. Also mention that Redis 6.2+ has a built-in `GETDEL` + `SET NX` pattern for distributed locking.

When to Choose Cache-Aside

SituationCache-Aside Good?Why
Read-heavy, write-infrequentYesCache hit rate is high; few invalidations
Data that can be slightly staleYesTTL-based consistency is sufficient
Unpredictable access patternsYesOnly popular data ends up in cache
Write-heavy workloadsNoConstant invalidation means low hit rate
Strong consistency requiredNoStale reads possible between write and re-populate
📌

Real-World Example: Twitter's Timeline Cache

Twitter uses Cache-Aside to store pre-computed timelines in Redis. When a user loads their feed, the app checks Redis first. On a miss, the fanout service assembles the timeline from Cassandra and writes it back. Because timelines can tolerate a few seconds of staleness, the TTL-based approach works perfectly without needing strong consistency.

📝

Knowledge Check

5 questions

Test your understanding of this lesson. Score 70% or higher to complete.

Ask about this lesson

Ask anything about Cache-Aside Pattern