Medium Database

Redis Caching Strategies: Cache-Aside vs Write-Through

Compare cache-aside and write-through caching with Redis, including consistency trade-offs, TTL strategy, and when to use each pattern.

22 Jul, 2026 4 min 7 Views 4 Code blocks
Diagram
graph TD A[Application] -->|Read| B{Cache-Aside} B -->|Hit| C[Return from Redis] B -->|Miss| D[Query Database] D --> E[Populate Redis] E --> C F[Application] -->|Write| G{Write-Through} G --> H[Write to Redis] H --> I[Write to Database] I --> J[Confirm to App]

Redis makes caching easy to bolt on, but the pattern you choose determines how consistent, resilient, and performant your system actually is. The two most common patterns — cache-aside and write-through — solve the same problem in fundamentally different ways.

Cache-Aside (Lazy Loading)

In cache-aside, the application talks to the cache directly, and the cache stays passive. On a read, the app checks Redis first; on a miss, it loads from the database and populates the cache. Writes go straight to the database, and the cache entry is invalidated (or updated) afterward.

def get_user(user_id):
    cached = redis.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)

    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    redis.setex(f"user:{user_id}", 3600, json.dumps(user))
    return user

def update_user(user_id, data):
    db.execute("UPDATE users SET ... WHERE id = %s", user_id)
    redis.delete(f"user:{user_id}")  # invalidate, don't update

Write-Through

In write-through, the cache sits in front of the database and owns the write path. Every write goes to the cache first, which synchronously writes through to the database before returning. Reads always come from the cache since it's guaranteed to be up to date.

def update_user(user_id, data):
    db.execute("UPDATE users SET ... WHERE id = %s", user_id)
    redis.setex(f"user:{user_id}", 3600, json.dumps(data))  # update, not invalidate
    return data

How the Two Patterns Differ

graph TD A[Application] -->|Read| B{Cache-Aside} B -->|Hit| C[Return from Redis] B -->|Miss| D[Query Database] D --> E[Populate Redis] E --> C F[Application] -->|Write| G{Write-Through} G --> H[Write to Redis] H --> I[Write to Database] I --> J[Confirm to App]

Consistency Trade-offs

Cache-aside has a small window where the cache can serve stale data: if two requests race between the database write and the cache invalidation, a reader might get an old value. Write-through avoids this because the cache and database are updated together in the same operation — but that comes at the cost of every write now waiting on two systems instead of one.

Cache-aside optimizes for read throughput and simplicity. Write-through optimizes for consistency at the cost of write latency.

TTL Strategy

Even with write-through, set a TTL as a safety net — bugs, race conditions, and out-of-band database writes (migrations, admin scripts) can all leave the cache holding stale data indefinitely without one.

CACHE_TTL_SECONDS = 3600  # 1 hour

redis.setex(key, CACHE_TTL_SECONDS, value)

A short TTL (minutes) suits volatile data like inventory counts or session state. A longer TTL (hours) suits data that changes rarely, like user profiles or product catalogs.

Handling Cache Stampedes

When a hot key expires, dozens of concurrent requests can all miss the cache at once and hammer the database simultaneously. A simple mitigation is a short-lived lock so only one request repopulates the cache while the rest wait:

def get_user(user_id):
    cached = redis.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)

    lock = redis.set(f"lock:user:{user_id}", "1", nx=True, ex=5)
    if lock:
        user = db.query("SELECT * FROM users WHERE id = %s", user_id)
        redis.setex(f"user:{user_id}", 3600, json.dumps(user))
        redis.delete(f"lock:user:{user_id}")
        return user
    else:
        time.sleep(0.05)
        return get_user(user_id)  # retry, likely a hit now

Write-Behind: A Third Option

Worth knowing, though outside this tip's main comparison: write-behind (write-back) caching writes to Redis immediately and asynchronously flushes to the database later. It gives the lowest write latency but risks data loss if the cache crashes before flushing — generally reserved for high-throughput analytics or logging pipelines, not systems of record.

When to Use Which

Reach for cache-aside when reads vastly outnumber writes and brief staleness is acceptable — product listings, user profiles, configuration data. Reach for write-through when correctness matters more than write speed and you can't tolerate cache/database drift — pricing data, inventory levels, anything downstream systems depend on being accurate.

Common Pitfalls

Forgetting to invalidate the cache on delete, not just update, leaves ghost entries that outlive the data they represent. Using a single global TTL for wildly different data types wastes memory on rarely-changing data and creates stampedes on frequently-changing data with the same expiry window. And treating Redis as a source of truth instead of a cache is a design smell — if losing the cache would lose data, it isn't a cache anymore.

The right caching strategy is the one whose failure mode you can live with: cache-aside fails toward staleness, write-through fails toward latency. Pick based on what your system can actually tolerate.

More from Database