The 4 Caching Strategies Every Developer Should Know
A practical guide to the 4 caching strategies — Cache-Aside, Write-Through, Write-Back, and Refresh-Ahead — with production failure modes, when not to cache, and a decision table.
Primary Focus
developmentAI Tools Covered
What You'll Learn
- ✓What Is a Cache?
- ✓Cache-Aside (Lazy Loading)
- ✓Write-Through
- ✓Write-Back (Write-Behind)
- ✓Refresh-Ahead
- ✓The Use-Case Mapping
Guide Curriculum
Cache Fundamentals
Learn key concepts
- •What Is a Cache?1m
The 4 Core Caching Strategies
Learn key concepts
- •Cache-Aside (Lazy Loading)1m
- •Write-Through1m
- •Write-Back (Write-Behind)1m
- •Refresh-Ahead1m
Choosing a Strategy
Learn key concepts
- •The Use-Case Mapping1m
- •The Rule of Thumb1m
When NOT to Cache
Learn key concepts
- •Cache Invalidation Traps2m
- •Stale Data Risk1m
- •Write-Heavy Workloads1m
Production Failure Modes
Learn key concepts
- •Cache-Aside — Thundering Herd2m
- •Write-Through — Write Amplification1m
- •Write-Back — Memory Pressure and Data Loss1m
- •Refresh-Ahead — Eviction Jitter1m
The Decision Table
Learn key concepts
- •Which Strategy for Which Use Case2m
Preview: First Lesson
Cache Fundamentals
What Is a Cache?
Before choosing a strategy, you need to know what a cache actually is and why it pays off only at scale. This module covers the basic model — hits, misses, and the order-of-magnitude speed difference that makes caching worth its complexity.
A cache is a fast, temporary copy of your most-used data stored in memory (like Redis) that sits between your application and your database. Instead of hitting the database on every request, your app checks the cache first. If data is there, that's a cache hit — instant. If not, that's a cache miss — you read from the database and store the result for next time.
The speed difference is dramatic:
- Database query: 50–500ms
- Cache read: 1–5ms
At 10 users, this doesn't matter. At 10,000 concurrent users, it's the difference between survival and collapse.
Start learning with this comprehensive guide
This guide includes:
About the Author
Hiram Clark is the founder of vybecoding.ai and editor of every guide and news article published on the site. He reviews all AI-drafted content for accuracy before publication and is personally accountable for factual errors. He works hands-on with the AI development tools, workflows, and infrastructure covered here.
Full Guide Content
Complete lesson text — start the interactive course above for exercises and progress tracking.
Module 1Cache Fundamentals
1.1What Is a Cache?
Before choosing a strategy, you need to know what a cache actually is and why it pays off only at scale. This module covers the basic model — hits, misses, and the order-of-magnitude speed difference that makes caching worth its complexity.
A cache is a fast, temporary copy of your most-used data stored in memory (like Redis) that sits between your application and your database. Instead of hitting the database on every request, your app checks the cache first. If data is there, that's a cache hit — instant. If not, that's a cache miss — you read from the database and store the result for next time.
The speed difference is dramatic:
- Database query: 50–500ms
- Cache read: 1–5ms
At 10 users, this doesn't matter. At 10,000 concurrent users, it's the difference between survival and collapse.
Module 2The 4 Core Caching Strategies
2.1Cache-Aside (Lazy Loading)
Each strategy makes a different tradeoff between read speed, write speed, consistency, and durability. This module walks through all four with working code, what each is best for, and the key tradeoff you accept when you choose it.
How it works:- App checks cache first.
- If miss: read from DB, write to cache, return data.
- If hit: return cached data directly.
async function getUser(userId: string) {
const cached = await redis.get(`user:${userId}`)
if (cached) return JSON.parse(cached)
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId])
await redis.setex(`user:${userId}`, 3600, JSON.stringify(user))
return user
}2.2Write-Through
- App writes to cache AND database simultaneously on every write.
- Reads always hit cache (data is always fresh).
async function updateCart(userId: string, cart: Cart) {
await Promise.all([
db.query('UPDATE carts SET data = $1 WHERE user_id = $2', [cart, userId]),
redis.setex(`cart:${userId}`, 86400, JSON.stringify(cart))
])
}2.3Write-Back (Write-Behind)
- App writes to cache only — immediately fast.
- A background worker flushes cache to the database asynchronously.
2.4Refresh-Ahead
- Cache proactively refreshes data before it expires.
- A background job checks TTLs and pre-loads the next version.
Module 3Choosing a Strategy
3.1The Use-Case Mapping
Knowing the four strategies is not enough — you need a fast way to map a workload to the right one. This module gives you a use-case table and the rule-of-thumb shortcuts that cover most real systems.
| Use Case | Recommended Strategy |
|---|---|
| User profiles | Cache-Aside |
| Shopping carts | Write-Through |
| Analytics counters | Write-Back |
| Homepage feed | Refresh-Ahead |
| Session tokens | Write-Through |
| Product catalog | Cache-Aside |
| Real-time leaderboard | Refresh-Ahead |
3.2The Rule of Thumb
- Read-heavy, unpredictable access → Cache-Aside
- Write-heavy, must stay consistent → Write-Through
- Write-heavy, speed over durability → Write-Back
- High-traffic, predictable reads → Refresh-Ahead
Most production systems combine strategies. User profiles use Cache-Aside. Carts use Write-Through. View counts use Write-Back. The homepage uses Refresh-Ahead.
Start with Cache-Aside. Add the others only when profiling shows a bottleneck.
Module 4When NOT to Cache
4.1Cache Invalidation Traps
Caching the wrong data is worse than not caching at all — it introduces correctness bugs that are harder to debug than pure slowness. This module covers the three situations where the right answer is to not cache, or to cache very carefully.
Cache invalidation is one of the two hard problems in computer science (the other being naming things). The moment you cache data that can change, you own the problem of keeping it synchronized. Common traps:- Partial updates: You cache a user object, then update only their email in the database. The cache still holds the old email. Any code reading from cache serves stale data until TTL expires.
- Cross-entity dependencies: Caching a blog post that embeds the author's name. Author changes their name in their profile. The post cache has no idea.
- Distributed invalidation: You have three app servers, each with a local in-process cache. One server invalidates its copy. The other two don't know. Users hitting those servers see stale data for minutes.
The fix is explicit invalidation on write — but that requires every write path to know about every cache key it affects. This coupling grows non-linearly as your data model gets more relational.
4.2Stale Data Risk
Some data is too expensive to serve stale, regardless of how short the TTL is:
- Inventory counts: A product showing "3 left" when it's actually sold out causes oversells and support tickets.
- Permissions and roles: A user you've just suspended can still act on cached auth data for minutes.
- Financial balances: A wallet balance that's 30 seconds stale is a liability, not an optimization.
- Legal or compliance data: Privacy settings, consent flags, and data-deletion records must reflect reality immediately.
For these, either skip the cache entirely or use a write-through + synchronous invalidation pattern with a TTL of zero (cache only within a single request lifecycle, not across requests).
4.3Write-Heavy Workloads
If a key is written more often than it is read, caching it is pure overhead. You're paying the cost of cache writes and the risk of staleness with no read speedup to justify it. A rough heuristic: if the read-to-write ratio is below 5:1, measure before caching. For ratios below 2:1, caching is probably net negative.
Module 5Production Failure Modes
5.1Cache-Aside — Thundering Herd
Each strategy has a characteristic failure pattern you will eventually encounter in production. This module names each one, explains exactly what goes wrong, and gives concrete prevention techniques you can apply before the 2am incident.
What happens: A popular key expires. Simultaneously, hundreds of requests check the cache, all get a miss, and all issue the same expensive database query at the same moment. The database receives a spike that can take it down. How to prevent it:- Mutex / lock on miss: The first request acquires a Redis lock (
SET lock:user:123 1 NX EX 5), fetches from DB, populates cache, releases lock. All other requests wait and then read from cache. - Probabilistic early expiration: Instead of expiring at a hard TTL, expire slightly early with a small random probability as the TTL approaches. This staggers refreshes across multiple requests.
- Jitter on TTL: When setting cache entries, add a random offset to the TTL (
3600 + Math.random() * 300). Prevents multiple keys from expiring at the exact same second.
async function getUserWithLock(userId: string) {
const cached = await redis.get(`user:${userId}`)
if (cached) return JSON.parse(cached)
const lockKey = `lock:user:${userId}`
const lockAcquired = await redis.set(lockKey, '1', 'NX', 'EX', 5)
if (!lockAcquired) {
await new Promise(resolve => setTimeout(resolve, 50))
const retried = await redis.get(`user:${userId}`)
if (retried) return JSON.parse(retried)
}
try {
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId])
const jitter = Math.floor(Math.random() * 300)
await redis.setex(`user:${userId}`, 3600 + jitter, JSON.stringify(user))
return user
} finally {
await redis.del(lockKey)
}
}5.2Write-Through — Write Amplification
- Wrap both writes in error handling. If the cache write fails, delete the key rather than leaving stale data:
await redis.del(key). The next read will repopulate it via Cache-Aside logic. - Use Redis
MULTI/EXEC(transaction) for multi-key updates so cache is either fully updated or not at all. - Accept that Write-Through adds latency proportional to your Redis round-trip (~1–3ms). If that's unacceptable, switch to Write-Back and accept the durability tradeoff explicitly.
5.3Write-Back — Memory Pressure and Data Loss
maxmemory limit and starts evicting keys — including dirty keys that haven't been flushed yet. Those writes are permanently lost with no error surfaced to the user.
How to prevent it:
- Set
maxmemory-policy noevictionfor Write-Back caches. Redis will return errors on new writes rather than silently evicting dirty data. Your app can then apply backpressure. - Maintain a dirty key registry — a Redis Set or sorted set that tracks which keys have unflushed writes. The flush worker reads from this set, not from key scans.
- Flush on a schedule and on a count threshold. If dirty key count exceeds 10,000 before the next scheduled flush, trigger an early flush.
5.4Refresh-Ahead — Eviction Jitter
- Size your Redis instance so eviction never happens during normal operation. Refresh-Ahead requires stable key residency — it is incompatible with a memory-constrained Redis running near capacity.
- Use
OBJECT FREQ(withmaxmemory-policy allkeys-lfu) to let Redis keep frequently-accessed keys resident. LFU eviction is significantly more cache-friendly for Refresh-Ahead than the default LRU policy. - Monitor
keyspace_missesin RedisINFO stats. A rising miss rate on keys that should always be warm is the first signal that eviction is disrupting your refresh cycle.
Module 6The Decision Table
6.1Which Strategy for Which Use Case
This final module consolidates everything into a single decision table keyed on the three variables that actually drive the choice — read/write ratio, staleness tolerance, and durability — plus how to read it under pressure.
| Use Case | Strategy | Read/Write Ratio | Staleness OK? | Durability Required? |
|---|---|---|---|---|
| User profile | Cache-Aside | High (100:1+) | Yes (seconds) | No |
| Shopping cart | Write-Through | Medium (5:1) | No | Yes |
| Session tokens | Write-Through | High (50:1) | No | Yes |
| View / like counts | Write-Back | Low (1:1) | Yes (seconds) | No |
| Homepage feed | Refresh-Ahead | Very high (1000:1) | No | No |
| Product catalog | Cache-Aside | Very high (500:1) | Yes (minutes) | No |
| Real-time leaderboard | Refresh-Ahead | Very high (200:1) | No | No |
| Inventory count | No cache / Write-Through (TTL=0) | Medium | No | Yes |
| User permissions / roles | Write-Through + invalidate on write | High | No | Yes |
| Analytics aggregates | Write-Back | Very low (1:10) | Yes (minutes) | No |
How to read this table:- If staleness is not OK and durability is required → Write-Through, or skip the cache.
- If read/write ratio is low (under 5:1) → measure before caching anything.
- If read/write ratio is very high and you control the write path → Refresh-Ahead gives the best read performance at the cost of infrastructure complexity.
- When in doubt → Cache-Aside. It's the safest default, fails gracefully, and is easy to reason about under load.