Redis Caching Patterns: Cache-Aside, Write-Through, and Write-Behind
Redis caching patterns are the small set of strategies that decide how your cache and your database stay in sync — and picking the wrong one is how teams end up serving stale prices, losing writes, or discovering that their "cache" made the system slower. The four patterns worth knowing are cache-aside, read-through, write-through, and write-behind. This guide explains each one, when to reach for it, and how to implement the most common one correctly. It is a supporting page in the Devgains database cluster, where the Postgres performance guide covers the database-side levers; caching is the layer you add in front of a database that is already indexed and pooled.
Quick answer: what are the main Redis caching patterns?
There are four core Redis caching patterns:
- Cache-aside (lazy loading): the application checks Redis first; on a miss it reads the database and populates the cache. The default for most systems.
- Read-through: the cache library sits in front of the database and loads missing entries for you, so the app only ever talks to the cache.
- Write-through: every write goes to Redis and the database synchronously, keeping them consistent at the cost of write latency.
- Write-behind (write-back): the app writes to Redis, which flushes to the database asynchronously — fast writes, but risk of data loss if Redis dies before the flush.
Rule of thumb: start with cache-aside and a TTL. Reach for the others only when a specific consistency or write-throughput requirement forces your hand.
Why caching patterns matter
A cache is not a magic "make it fast" button — it is a second copy of your data, and two copies of anything raise the same hard question: what happens when they disagree? The pattern you choose is really a decision about who is responsible for keeping the copies consistent, and what you are willing to lose when a node fails.
Get it right and Redis absorbs the bulk of your read traffic in microseconds, shielding a database that would otherwise thrash under load — the same collapse described in why your database falls over at 100 users. Get it wrong and you introduce a subtle new failure mode: stale reads, lost writes, or a "thundering herd" that hammers the database the instant a hot key expires.
Architecture: the four patterns at a glance
The patterns split along two axes — who populates the cache on a read, and when the database is updated on a write.
READS WRITES
───── ──────
Cache-aside app → cache → (miss) Write-through app → cache → db (sync)
app → db → cache Write-behind app → cache ⇢ db (async)
Read-through app → cache → db
(cache does the load)Reads and writes are chosen independently. A very common production setup is cache-aside for reads + write-through for writes, so the database is always current and the cache is refreshed lazily.
Step-by-step: implementing cache-aside
Cache-aside is the pattern you will use 80% of the time, so implement it well. The read path is: look in Redis, and only touch the database on a miss.
import json
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
TTL_SECONDS = 300 # 5 minutes
def get_user(user_id: int) -> dict:
key = f"user:{user_id}"
# 1. Try the cache first.
cached = r.get(key)
if cached is not None:
return json.loads(cached) # cache hit
# 2. Cache miss: read the source of truth.
user = db.query_user(user_id) # your DB call
# 3. Populate the cache with an expiry, then return.
r.set(key, json.dumps(user), ex=TTL_SECONDS)
return userThree details make or break this code:
- Always set a TTL (
ex=). Without expiry, entries live forever and drift out of sync with the database. TTL is your safety net for the writes you forget to invalidate. - Cache the miss too, sometimes. For keys that are frequently requested but absent, storing a short-lived sentinel prevents every miss from hitting the database (cache penetration).
- Namespace your keys (
user:42, not42). It keeps eviction, debugging, andSCANsane.
On the write path, the simplest correct approach is invalidate on write: update the database, then delete the cache key so the next read repopulates it.
def update_user(user_id: int, changes: dict) -> None:
db.update_user(user_id, changes) # 1. write source of truth
r.delete(f"user:{user_id}") # 2. invalidate; next read reloadsDeleting is safer than updating the cached value in place: it avoids a race where two concurrent writers leave the cache holding a value that never existed in the database.
Write-through and write-behind in practice
When invalidate-on-write is not enough, the two write patterns trade latency against durability.
Write-through keeps Redis and the database consistent by writing to both before returning:
async function saveConfig(key: string, value: object): Promise<void> {
// Write to the database first — it is the source of truth.
await db.upsertConfig(key, value);
// Then write to the cache synchronously, so the next read is warm and correct.
await redis.set(`config:${key}`, JSON.stringify(value), "EX", 600);
}Use write-through when reads must never be stale and write volume is modest — feature flags, configuration, pricing tables. The cost is added write latency, and if either store is slow, your writes slow with it.
Write-behind inverts the priority: the app writes only to Redis, and a background worker flushes batches to the database. Writes become near-instant and the database sees smoothed, batched load — ideal for high-volume counters, metrics, or "last seen" timestamps where losing a few seconds of the newest data on a crash is acceptable. The danger is explicit: if Redis dies before the flush, that data is gone. Never put write-behind in front of data you cannot afford to lose, such as payments.
TTL and eviction: the part everyone forgets
Redis is memory-bound, so you must decide what happens when it fills up. Set a maxmemory limit and
an eviction policy explicitly — the defaults are rarely what you want for a cache.
# redis.conf — treat Redis as a bounded cache, not an unbounded store
maxmemory 2gb
maxmemory-policy allkeys-lru # evict least-recently-used keys across the whole keyspaceCommon policies:
allkeys-lru— evict the least-recently-used key. The sensible default for a pure cache.allkeys-lfu— evict the least-frequently-used key. Better when a small hot set dominates.volatile-ttl— among keys with a TTL, evict the one expiring soonest.noeviction— reject writes when full. Correct for a datastore, dangerous for a cache.
Pair eviction with sensible TTLs and add jitter (randomize the TTL by ±10%) so thousands of keys do not expire in the same second and stampede the database — the cache equivalent of the N+1 query problem, where one logical operation quietly fans out into many.
Comparison table: which Redis caching pattern to use
| Pattern | Read path | Write path | Consistency | Best for | Main risk |
|---|---|---|---|---|---|
| Cache-aside | App loads on miss | Invalidate on write | Eventual (TTL-bounded) | General-purpose reads | Stale until TTL/invalidate |
| Read-through | Cache loads on miss | (paired with a write pattern) | Eventual | Clean app code, shared cache logic | Cache library lock-in |
| Write-through | Warm after write | Sync to cache + DB | Strong on the cached key | Config, flags, pricing | Higher write latency |
| Write-behind | Warm after write | Async flush to DB | Weak (until flush) | Counters, metrics, high write volume | Data loss on crash |
If you remember one row, remember the first: cache-aside with a TTL is the right starting point for the overwhelming majority of applications.
Best practices
- Default to cache-aside + TTL. Add complexity only when a measured requirement demands it.
- Invalidate by deleting, not updating, to avoid write-write races on the cached value.
- Always bound memory with
maxmemoryand an LRU/LFU policy — an unbounded cache is a future outage. - Add TTL jitter to prevent synchronized expiry stampedes on hot keys.
- Cache the right granularity. Cache computed results and hot rows, not entire result sets you rarely reuse.
- Never cache what you cannot lose. Redis is a cache, not your system of record — the database behind it, properly indexed and pooled, remains the source of truth.
Common mistakes
- No TTL. Entries live forever and silently drift out of sync. Every cache-aside entry needs an expiry.
- Updating the cache in place on writes. Two concurrent writers can interleave and leave a value that was never in the database. Delete the key instead.
noevictionon a cache. When memory fills, writes start failing and the "fast path" becomes an outage.- Caching to hide bad queries. A cache in front of an unindexed table just delays the pain. Fix the indexes and query plans first, then cache.
- Ignoring the thundering herd. When a hot key expires, thousands of requests miss at once. Use jitter, request coalescing, or a short lock so only one request rebuilds the entry.
Takeaways
- The four Redis caching patterns are cache-aside, read-through, write-through, and write-behind — reads and writes are chosen independently.
- Cache-aside with a TTL is the default; it fails safe (Redis down = slower, not broken).
- Write-through buys strong consistency at the cost of write latency; write-behind buys write throughput at the cost of durability.
- Always set
maxmemory+ an eviction policy and add TTL jitter — memory and stampedes are where naive caches break. - A cache is a second copy of your data. Choose the pattern by asking what you are willing to lose when a node fails.
FAQ
What is the most common Redis caching pattern? Cache-aside (also called lazy loading) is by far the most common. The application checks Redis first; on a miss it reads the database, stores the result with a TTL, and returns it. It is simple, resilient to cache outages, and correct for the majority of read-heavy workloads.
What is the difference between write-through and write-behind caching? Write-through updates the cache and the database synchronously on every write, so both stay consistent but writes are slower. Write-behind writes only to the cache and flushes to the database asynchronously, so writes are fast but recent data can be lost if the cache node fails before the flush.
How do I keep a Redis cache from serving stale data? Set a TTL on every entry so stale values self-expire, and invalidate keys on write by deleting them so the next read repopulates from the database. For data that must never be stale, use write-through so the cache is updated in lockstep with the database.
What eviction policy should I use for a Redis cache?
For a general-purpose cache, use allkeys-lru (evict least-recently-used). If a small set of keys is
accessed far more often than the rest, allkeys-lfu (least-frequently-used) usually gives a better
hit rate. Always pair it with an explicit maxmemory limit.
Does caching replace database indexing? No. A cache reduces repeated reads of the same data, but a cache miss still hits the database. If those underlying queries are slow, you have just added a layer that delays the pain. Fix indexes and query plans first, then cache what remains hot.
Conclusion
Redis caching patterns come down to two decisions — how a read fills the cache, and when a write reaches the database. Start with cache-aside plus a TTL: it is simple, it fails safe, and it handles the overwhelming majority of workloads. Move to write-through only when reads must never be stale, and to write-behind only when write throughput matters more than the newest few seconds of data. Bound your memory, set an eviction policy, and add TTL jitter, and Redis becomes the layer that lets an already-tuned database breathe. From here, continue with connection pooling and the Postgres performance pillar, or browse the full database category. Caching also powers LLM systems — see prompt caching for LLM cost for the same idea applied to model inference.
References
- Redis documentation — Caching — official overview of Redis as a cache, patterns, and eviction.
- Redis — Key eviction — how
maxmemoryand the LRU/LFU/TTL policies behave. - AWS — Database Caching Strategies Using Redis — cache-aside, write-through, and related patterns in depth.
- Microsoft — Cache-Aside pattern — the canonical description of lazy-loading cache-aside.

