Redis Caching Strategies

Caching means storing a copy of expensive-to-fetch data in Redis so your app can read it quickly without hitting your database every time. The strategy you choose determines when the cache gets updated, how fresh the data stays, and what happens when the cache runs out of space.

The Cafeteria Pre-Cook Analogy

A cafeteria that cooks every dish from scratch for each customer would be painfully slow at lunch hour. Instead, the kitchen pre-cooks popular meals, stores them in warming trays (cache), and serves directly from there. New items replace old ones when the trays fill up. Redis works the same way for your app and database.

Without Cache:
  User request ──▶ App ──▶ Database (slow disk query) ──▶ App ──▶ User
  Every user waits for the full database round trip.

With Cache:
  User request ──▶ App ──▶ Redis (RAM, fast)
                              │
                   cache HIT ─┴──▶ return data immediately (no DB)
                   cache MISS ────▶ query DB ──▶ store in Redis ──▶ return

Strategy 1: Cache-Aside (Lazy Loading)

The app checks Redis first. On a miss, it fetches from the database and stores the result in Redis for next time. The cache only holds data that was actually requested.

                  App receives request for user:1001

                        │
                        ▼
              GET user:1001 from Redis
                        │
          ┌─────────────┴────────────┐
       HIT │                         │ MISS
          ▼                         ▼
   Return cached data       Query database
                                    │
                                    ▼
                           HSET user:1001 ... (store in Redis)
                           EXPIRE user:1001 300
                                    │
                                    ▼
                             Return data to app

Code flow:
  data = GET user:1001
  if data is nil:
    data = db.query("SELECT * FROM users WHERE id=1001")
    HSET user:1001 name data.name email data.email
    EXPIRE user:1001 300   ← cache for 5 minutes
  return data

Cache-Aside is the most common pattern. It only loads data users actually need, so the cache stays relevant.

Strategy 2: Write-Through

Every time the app writes data, it writes to both the database and Redis at the same time. The cache never goes stale because updates go to both places together.

App updates user profile:

  Write to DB:     UPDATE users SET city="Jaipur" WHERE id=1001
  Write to Redis:  HSET user:1001 city "Jaipur"

  Both writes happen in the same code path.
  Next read: cache is already up-to-date. No miss.

Trade-off: every write is slightly slower (two writes instead of one).
Benefit: cache always holds fresh data.

Strategy 3: Write-Behind (Write-Back)

The app writes only to Redis immediately and queues database writes for later. A background process flushes data to the database asynchronously. This makes writes very fast at the cost of some durability risk.

User updates profile:

  App ──▶ Redis (instant write) ──▶ returns "OK" to user
               │
               ▼
         Background worker (runs every few seconds)
               │
               ▼
         Database (batched writes, less load)

Risk: If Redis crashes before the background worker flushes,
those writes are lost.

Use when: write speed matters more than strict durability
          (analytics events, view counts, etc.)

Strategy 4: Read-Through

The cache itself is responsible for fetching missing data. The app always talks to the cache. When the cache misses, the cache layer (not the app) calls the database and populates itself. The app code stays simple.

App ──▶ Redis cache layer ──▶ returns data

             Cache layer internally:
             if key not found:
               fetch from DB
               store in Redis
               return to app

The app does not know whether data came from Redis or DB.
Complexity moves from app code into the cache layer/library.

Cache Eviction – What Happens When Redis Runs Out of Space

Redis can hold data only up to the memory limit you set. When that limit is reached, Redis uses an eviction policy to decide which keys to remove.

Set maximum memory in redis.conf:
  maxmemory 256mb
  maxmemory-policy allkeys-lru

Eviction policies:
┌──────────────────────┬────────────────────────────────────────────┐
│  Policy              │  What Gets Removed                         │
├──────────────────────┼────────────────────────────────────────────┤
│  noeviction          │  Returns an error. Nothing is removed.     │
│  allkeys-lru         │  Least recently used key across all keys   │
│  volatile-lru        │  Least recently used key with an expiry    │
│  allkeys-lfu         │  Least frequently used across all keys     │
│  allkeys-random      │  Random key from all keys                  │
│  volatile-random     │  Random key that has an expiry set         │
│  volatile-ttl        │  Key with the shortest remaining TTL       │
└──────────────────────┴────────────────────────────────────────────┘

LRU (Least Recently Used): kick out keys not accessed in a long time.
LFU (Least Frequently Used): kick out keys that are rarely accessed.
Visual: Redis at max memory with allkeys-lru

  Key A ──── last accessed 2 hours ago   ← evicted first
  Key B ──── last accessed 1 hour ago
  Key C ──── last accessed 5 minutes ago
  Key D ──── last accessed 10 seconds ago ← kept (most recent)

New key arrives → Redis evicts Key A → stores new key

Cache Stampede – The Thundering Herd Problem

When a popular cache key expires, hundreds of requests arrive at the same moment, all see a miss, and all try to rebuild the cache simultaneously. This hammers the database.

Cache stampede scenario:
  Cached homepage expires at 12:00:00
  1000 users visit at 12:00:01
  All 1000 see cache miss
  All 1000 query the database at once → DB collapses

Prevention approach – Probabilistic Early Expiry:
  When TTL is below a threshold, randomly rebuild the cache
  BEFORE it expires. Only one request rebuilds; others still
  read the existing cached value.

Simpler prevention: use a lock (SETNX) while rebuilding:
  SETNX rebuild:homepage "1" EX 10
  If lock acquired → rebuild cache
  If lock not acquired → wait briefly and retry GET

Key Points

  • Cache-Aside (lazy loading) is the most common pattern — fetch from DB on miss, store in Redis, set a TTL.
  • Write-Through keeps cache and DB in sync by writing to both on every update.
  • Write-Behind writes to Redis first and flushes to DB later — faster writes, small durability risk.
  • Set maxmemory and an eviction policy so Redis handles full memory gracefully instead of crashing.
  • allkeys-lru is a safe default for a general-purpose cache.
  • Prevent cache stampedes using a SETNX lock or probabilistic early expiry.

Leave a Comment