Versus

Caching Strategies Compared: Cache-Aside, Write-Through, and Write-Behind

How each pattern moves data between your app, cache, and database, and how to pick the right one for your read/write mix.

·
4 min read
·
AI-assisted

AI tooling disclosure

Generative AI materially contributed to this article.

Human review confirmed by the author.

Caching Strategies Compared: Cache-Aside, Write-Through, and Write-Behind

Every cache forces you to answer one question: when data changes, who updates the cache, and when? Reads are the easy half. The write path is where caches go stale, lose data, or slow your app down.

Three patterns cover most real-world systems. In each, the app talks to a cache (Redis, Valkey, Memcached) that sits in front of a database.

1. Cache-aside (lazy loading)

The app owns the logic. On a read, it checks the cache first. On a miss, it loads from the database, stores the result in the cache with a TTL, and returns it.

sequenceDiagram
    participant App
    participant Cache
    participant DB
    App->>Cache: GET user:42
    alt cache hit
        Cache-->>App: value
    else cache miss
        Cache-->>App: nil
        App->>DB: SELECT user 42
        DB-->>App: row
        App->>Cache: SET user:42 with TTL
    end

On a write, the app updates the database and then deletes the cached key. The next read repopulates it.

Strengths

  • Simple, and it works with any cache.

  • Only data that is actually requested gets cached.

  • If the cache goes down, the app still works, just slower, as long as the database can absorb the full read load.

Watch out for

  • The first request after a cache miss is slower, and a cold cache can cause many misses at once.

  • There is a window of staleness between a database write and the cache refresh. A race is possible too: a reader fetches the old row, a writer updates the database and deletes the key, then the reader writes the old value back. A short TTL bounds how long the stale value survives. For stricter guarantees, use versioned values, short-lived locks (leases) on the miss path, or a delayed second delete after the write.

  • Popular keys expiring together can cause a thundering herd. Add jitter to TTLs and coalesce concurrent loads for the same key.

2. Write-through

Every write goes through the cache, and the cache writes to the database synchronously before acknowledging. Reads of recently written data then find fresh values in the cache.

sequenceDiagram
    participant App
    participant Cache
    participant DB
    App->>Cache: SET user:42 new value
    Cache->>DB: write user 42
    DB-->>Cache: ok
    Cache-->>App: ok

Strengths

  • The cache and database agree after a successful write, unless a partial failure or concurrent writer interferes.

  • The read path is trivial, and hit rates are high for data that has been written.

Watch out for

  • Write latency is the cache write plus the database write.

  • You cache data that may never be read again, which wastes memory.

  • If the database write fails, you must undo or skip the cache update.

  • Two concurrent writers to the same key can interleave so the cache and database end up with different values. Serialize writes per key or attach a version number.

Write-through is usually paired with a read-through layer, where the cache itself loads missing data, so the app only ever talks to the cache. Redis and Valkey don't do this on their own, so in practice it lives in your application code or a caching library.

3. Write-behind (write-back)

The app writes to the cache, gets an immediate acknowledgment, and the cache flushes changes to the database asynchronously, often in batches.

sequenceDiagram
    participant App
    participant Cache
    participant Buffer as Write buffer
    participant DB
    App->>Cache: SET user:42 new value
    Cache-->>App: ok (fast)
    Cache->>Buffer: enqueue change
    Note over Buffer,DB: flushed later in batches
    Buffer->>DB: batch write
    DB-->>Buffer: ok

Strengths

  • It has the lowest write latency of the three.

  • Multiple updates to the same key can be coalesced into one database write.

  • It absorbs write spikes and reduces database load.

Watch out for

  • If an acknowledged write exists only in volatile memory and the cache or buffer fails before it is flushed, that write is lost. Durable queues and replication reduce this risk, but they add complexity and don't eliminate it.

  • The database lags behind the cache, so anything reading the database directly (reports, other services) sees stale data.

  • Retries, ordering, and failure handling add real complexity.

This fits data where losing a few seconds is acceptable, such as view counters, activity logs, and telemetry. It is a poor fit for payments or inventory.

Side-by-side

Cache-aside

Write-through

Write-behind

Who manages the cache

App

Cache layer or app library

Cache layer or app library

Write latency

Database + a cache delete

Cache + database

Cache only

Freshness after a write

Fresh after invalidation; stale if an invalidation is missed or raced (TTL bounds it)

Fresh after both updates succeed

Fresh in cache, stale in database

Data loss risk

None

None

Yes, if the cache fails before a flush (durable buffers reduce this)

Complexity

Low

Medium

High

Best for

Read-heavy workloads

Read-heavy data that must stay consistent

Write-heavy, loss-tolerant data

Choosing one

flowchart TD
    A["Are writes heavy, and can you tolerate delayed database updates and losing unflushed writes?"]
    A -->|Yes| WB["Write-behind"]
    A -->|No| B["Do you need stronger freshness than invalidate-on-write gives?"]
    B -->|Yes| WT["Write-through"]
    B -->|"No, brief staleness is fine"| CA["Cache-aside"]

Practical tips

  • Default to cache-aside. It is the simplest and the most forgiving when something fails.

  • Always set a TTL as a safety net, even when you invalidate explicitly. It limits how long a bug or a missed invalidation can hurt you. With write-through, make sure an expired key can be reloaded from the database.

  • For cache-aside, delete on write rather than update. It stops concurrent writers from overwriting each other with out-of-order values. It doesn't stop a slow reader from caching an old value, so pair it with a TTL, and with versioning or leases if staleness is costly. Write-through and write-behind update the cache by design.

  • Mix patterns. Many systems use cache-aside for most data, write-through for a small set of hot, critical records, and write-behind for counters.

  • Watch your hit ratio and your database load. A cache that isn't absorbing reads is just extra latency.

Wrapping up

None of these patterns is "best." Each trades freshness, latency, and durability against complexity. Start with cache-aside, move to write-through where staleness hurts, and reach for write-behind only when write volume demands it and you can live with the risk.

Comments(0)

Join the discussion by logging into your account.

No comments yet. Be the first to comment!

Anshu Pathak

Passionate developer sharing knowledge about modern web technologies and best practices.

Subscribe to Anshu Pathak's Newsletter

Direct email dispatches when new stories are published. Zero algorithms.