The 4 Caching Strategies Every Developer Should Know

Advanced18m readFull-stack developers

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

development

AI Tools Covered

programmingdatabasestutorial

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

1 lessons
  • What Is a Cache?1m

The 4 Core Caching Strategies

Learn key concepts

4 lessons
  • Cache-Aside (Lazy Loading)1m
  • Write-Through1m
  • Write-Back (Write-Behind)1m
  • Refresh-Ahead1m

Choosing a Strategy

Learn key concepts

2 lessons
  • The Use-Case Mapping1m
  • The Rule of Thumb1m

When NOT to Cache

Learn key concepts

3 lessons
  • Cache Invalidation Traps2m
  • Stale Data Risk1m
  • Write-Heavy Workloads1m

Production Failure Modes

Learn key concepts

4 lessons
  • 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

1 lessons
  • 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.


Free Access

Start learning with this comprehensive guide

This guide includes:

6 modules with 15 lessons
18m estimated reading time

About the Author

H
✨ Vibe Coder
@hiram-clark

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:
  1. App checks cache first.
  2. If miss: read from DB, write to cache, return data.
  3. If hit: return cached data directly.
Best for: Read-heavy workloads with unpredictable access patterns (user profiles, product pages). Key tradeoff: First request always hits the database. Cold starts are slow.
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

How it works:
  1. App writes to cache AND database simultaneously on every write.
  2. Reads always hit cache (data is always fresh).
Best for: Write-frequent data that must stay consistent (shopping carts, session state). Key tradeoff: Every write takes twice as long. Cache fills with data that may never be read.
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)

How it works:
  1. App writes to cache only — immediately fast.
  2. A background worker flushes cache to the database asynchronously.
Best for: Write-intensive workloads where speed matters more than immediate durability (analytics counters, view counts, real-time scoring). Key tradeoff: If the cache crashes before the flush, you lose data. Not suitable for financial transactions.

2.4Refresh-Ahead

How it works:
  1. Cache proactively refreshes data before it expires.
  2. A background job checks TTLs and pre-loads the next version.
Best for: Predictable, high-traffic data where stale data is unacceptable (homepage feed, trending content, leaderboards). Key tradeoff: Requires knowing what data will be needed. Background refresh adds infrastructure complexity.

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

What happens: Every write goes to both the database and cache. Under write load, this doubles the number of I/O operations. More critically, if the database write succeeds but the cache write fails (network blip, Redis OOM), you now have inconsistency — the DB has the new value, the cache has the old one. How to prevent it:
  • 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

What happens: Writes accumulate in Redis faster than the background worker can flush them. Under extreme load, Redis hits its 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 noeviction for 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

What happens: The background refresh job assumes keys will still be in cache when it runs. Under memory pressure, Redis evicts a key between the job's check and its write. The job refreshes a key that no longer exists, the cache holds the new value briefly, then it gets evicted again. Meanwhile, requests that expect cache hits get misses, repeatedly hitting the database in a pattern that looks like a slow thundering herd. How to prevent it:
  • 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 (with maxmemory-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_misses in Redis INFO 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.