Caching Patterns: Cache-Aside, Write-Through, Write-Behind, and Read-Through15 min read
Module F-12·15 min read
The four caching strategies with their consistency guarantees, failure modes, and implementation trade-offs. Cache stampede prevention, invalidation timing, cache warm-up, and the decision framework for choosing the right pattern.
Who this module is for: You use Redis as a cache — you call redis.get(), fall back to the database on miss, and call redis.set(). That is the cache-aside pattern, and it is correct for many situations. But it is not the only pattern, and it is not always the right one. This module covers the four caching strategies with their trade-offs, consistency implications, and the failure modes most tutorials skip.
A cache sits between your application and your primary data store (typically a database). Its job is to answer repeated, expensive lookups cheaply by storing previously computed or fetched results in fast memory.
Every caching strategy makes a trade-off across three dimensions:
Consistency — how closely the cache matches the source of truth at any given moment
Complexity — how much application code is required to maintain the cache
Failure behavior — what happens when the cache is empty, stale, or unavailable
Cache stampede (thundering herd): If a popular key expires, many concurrent requests simultaneously see a miss and all rush to the database. The database receives N copies of the same expensive query. Solutions:
Probabilistic early expiry (PER): Recompute when TTL < threshold × random(). Some requests recompute before the key expires, warming the cache before the stampede.
Mutex / distributed lock: The first miss acquires a lock, computes, and populates the cache. Others wait and then read from the cache.
const cached = await redis.get(key); if (cached) return JSON.parse(cached);
// Try to acquire lock (NX = only if not exists, EX = expire in 5s) const locked = await redis.set(lockKey, '1', 'NX', 'EX', 5);
if (locked) { // We have the lock — compute and cache const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]); await redis.set(key, JSON.stringify(user), 'EX', 300); await redis.del(lockKey); return user; } else { // Another process is computing — wait briefly and retry await sleep(50); return getUserWithLock(userId); } }
Stale data: After an invalidation (DEL), the next request fetches fresh data from the database. But if you forget to invalidate after a write, the cache serves stale data until TTL expires.
Cache miss on cold start: After a deployment or Redis restart, the cache is empty. All requests hit the database simultaneously. Mitigate with cache warming (pre-populate common keys on startup).
Write-through keeps the cache synchronously updated on every write. The application writes to the cache and the database together — the write is only acknowledged when both succeed.
Write latency doubles: Every write waits for both the database and Redis to acknowledge. If Redis is slow, all writes are slow.
Cache pollution: You write to the cache whether or not the data is ever read again. Short-lived objects (created once, never read) consume cache memory unnecessarily. Mitigate with appropriate TTLs.
Partial failure: If the database write succeeds but the Redis write fails (network blip), the cache is stale. If the database write fails but Redis succeeds, the cache is incorrect. Neither is catastrophic for cache-aside (you can retry), but for write-through, you should treat the database as the source of truth and consider the cache write a best-effort update.
Write-behind acknowledges the write after updating only the cache. The cache write to the database happens asynchronously — in a background job or queue.
Writes go to Redis immediately. A background worker (queue consumer, cron job, or Redis keyspace notification listener) propagates changes to the database asynchronously.
Data loss on Redis crash: If Redis loses data (no persistence configured, or between AOF flushes) before the background job writes to the database, the writes are permanently lost. This is the most dangerous failure mode in caching. Only use write-behind when data loss is acceptable.
Ordering: The background job must process writes in order. If write B overwrites write A, but the background job processes B before A, the database ends up with stale data. Use a queue (Redis List or Stream) to ensure ordering.
Complexity: The background flush mechanism is additional infrastructure to build, test, and operate.
Read-through is cache-aside with the cache itself responsible for loading from the database on a miss — the application talks only to the cache abstraction, which handles database fallback internally.
The API is identical to cache-aside from the application's perspective:
const user = await cache.get('user:1001');
The difference is implementation: the cache layer is a wrapper that handles the miss logic itself. Libraries like cache-manager (Node.js) implement this pattern.
import cacheManager from 'cache-manager'; import redisStore from 'cache-manager-redis-yet';
// Application code — no explicit miss handling const user = await cache.wrap('user:1001', async () => { return db.query('SELECT * FROM users WHERE id = $1', [1001]); });
cache.wrap checks the cache, calls the factory function on a miss, stores the result, and returns it — all transparently.
Delete the cache key immediately after the corresponding database row is written:
await db.query('UPDATE products SET price = $2 WHERE id = $1', [productId, newPrice]); await redis.del(`product:${productId}`);
Risk: The DEL and the UPDATE are not atomic. A crash between the two leaves the cache stale. Mitigate by doing DEL before the UPDATE (stale but empty is safer than stale with wrong data) or by using a transaction with a message queue.
After a Redis restart or a new cache cluster deployment, the cache is empty. Every request is a miss until the cache fills — this is a "cold start." For high-traffic systems, a cold cache means a temporary database overload.
Warm-up strategies:
Eager warm-up — on startup, fetch the most-accessed keys from the database and pre-populate the cache
Shadow traffic — replay recent production traffic against the new cache instance before routing live traffic
Gradual rollout — route 5% of traffic to the new Redis instance, let it warm, then gradually increase
Cache-Aside — read from cache, fall back to DB on miss, write to DB and invalidate cache on writes. Default pattern. Simple, resilient, eventual consistency.
Write-Through — write to both cache and DB synchronously. Strong read consistency after writes. Higher write latency. Good for sessions and balances.
Write-Behind — write to cache immediately, flush to DB asynchronously. Lowest write latency. Risk of data loss on Redis crash.
Read-Through — cache handles its own miss logic. Clean application code. Same consistency as cache-aside.
Cache stampede — expired popular key → N concurrent DB queries. Mitigate with mutex locks or probabilistic early expiry.
Cache invalidation — proactive DEL after DB writes, or TTL-based expiry. Neither is perfectly safe under concurrent modification.
Cache warm-up — pre-populate after cold start to protect the database from the thundering herd of cache misses.
This completes the Foundation tier. You have covered the full Redis data model, all major data structures and their encodings, TTL and eviction mechanics, HyperLogLog/Bitmaps/Geo, pipelining, Pub/Sub, Streams, memory internals, transactions, and caching patterns.
The Practitioner tier begins with the infrastructure questions: how does Redis persist data to survive a restart?
Next: P-1 — RDB Snapshots: Point-in-Time Persistence — how Redis writes its entire in-memory dataset to disk, the fork-and-copy-on-write mechanism, snapshot configuration, and when RDB is the right (and wrong) persistence strategy.
Knowledge Check
A high-traffic news website uses the Cache-Aside pattern for its articles. When a breaking news article's cache key expires (TTL = 5 minutes), the database suddenly receives 500 identical complex queries simultaneously, causing a latency spike. What is this phenomenon called, and how can it be mitigated?
An e-commerce system implements the Write-Through caching pattern for user shopping carts. What is the primary operational trade-off of this specific pattern compared to Cache-Aside?
An analytics platform tracks real-time page views. It uses a Write-Behind (Write-Back) caching strategy, where it increments counters directly in Redis and a background worker flushes these counts to PostgreSQL every 5 minutes. If the Redis instance experiences a sudden hardware failure and restarts, what is the impact on the system?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.