Skip to main content
PRISM

Write strategies

intermediate · commonly asked

Write-through, write-back and write-around, with a crash injected on a timer so "how much did that lose" is a number on the screen rather than an argument.

Loading the simulation

The problem it solves

Caching discussions are almost entirely about reads. The write path gets a sentence — “and we invalidate on write” — and then the conversation moves on. That sentence is where the data loss lives.

Once a cache exists, a write has two possible destinations and the order matters. Write to both, and one of them can fail. Write to the cache and defer the database, and a crash loses whatever had not been flushed. Write to the database and skip the cache, and the next reader takes a guaranteed miss. There is no option that is fast, durable and simple at the same time, and choosing between them is choosing which of those three you are willing to give up.

The mechanism

Write-through writes the cache and the database synchronously before acknowledging. Every write pays the database’s latency, and the cache is never ahead of the store. A crash loses nothing. The cache is warm for the key you just wrote, which is useful if it is about to be read and wasteful if it is not.

Write-back (write-behind) writes the cache, acknowledges immediately, and flushes to the database later — on a timer, on eviction, or when a batch fills. Writes become as fast as memory, and multiple writes to the same key collapse into one database write, which can cut database load by an order of magnitude on hot keys. The cost is exact and quantifiable: a crash loses every write since the last flush.

Write-around writes only the database and invalidates (or simply does not populate) the cache. Nothing is ever stale, nothing is ever lost, and the key you just wrote is a guaranteed miss for the next reader. For write-heavy data that is rarely read back, this is the correct choice and the other two are wasting memory on entries nobody wants.

What the simulation shows

A crash is injected on a timer, so “how much did that lose?” is a number on screen rather than an argument.

Run the default write-through and watch the crash arrive: the unflushed-writes panel is flat at zero throughout, and the crash costs nothing. Now look at what it cost to get there — every write in the latency panel carries the database’s write time.

Switch to write-back. Write latency drops by orders of magnitude, because an acknowledgement now comes from memory. Then the unflushed-writes panel starts to climb between flushes — a sawtooth whose peak is your exposure — and the crash takes the whole tooth with it. Database load falls too, visibly, because repeated writes to the same key coalesce into one flush.

The exposure is a dial, not a mystery. Shorten the flush interval to 250 ms and the sawtooth shrinks in proportion: eight times more frequent flushes, roughly an eighth of the loss, and correspondingly less write coalescing. That is the entire trade, and it is continuous — you are not choosing between durable and fast, you are choosing a point on a line and should be able to say which point and why.

Finally, write-around: nothing lost, nothing stale, and the miss rate on recently written keys is 100%.

The numbers worth carrying

For write-back, exposure = flush interval × write rate. At 400 writes a second and a 2-second flush, a crash loses 800 writes. Say that number in the design review; “we use write-back” is not a decision, “we accept losing up to 800 writes” is.

For write-through, the cost is the database write on the critical path of every write — typically 5–20 ms against sub-millisecond memory, so write latency is set entirely by the store. If writes are 20% of traffic, that is 20% of requests paying full database latency, and the utilisation curve says what that does to the pool holding them.

For write-around, the cost is one guaranteed miss per written key. If the read-after-write pattern is common — a user editing their profile then viewing it — that miss is on the critical path of the very next request and users will feel it.

Where it breaks down

Dual writes are not atomic. Write-through writes two systems with no transaction between them. Cache succeeds, database fails: you have a cache entry for data that does not exist. Database succeeds, cache write fails: you have a stale entry with a TTL’s worth of life left. This is the same dual-write problem as the outbox pattern, and the practical mitigation is the same in spirit: make the database the source of truth, write it first, and treat the cache as derived — invalidate rather than update, so a failed invalidation is bounded by TTL rather than being wrong forever.

Invalidate versus update. Updating the cache on write races: two concurrent writers can interleave such that the cache ends up holding the older value permanently. Deleting the entry has no such race — the next reader repopulates from the source of truth. Delete, do not set, unless you have thought hard about the interleaving.

Write-back needs an ordered, replayable buffer to be more than a toy. What real systems call write-back is usually a durable log (a WAL, a Kafka topic) plus an asynchronous applier — at which point the loss window is the log’s own durability, not the cache’s, and the number above no longer applies. If you are proposing write-back, say whether the buffer is durable, because it changes the answer completely.

Multi-tier caches multiply the problem. A local cache in front of a shared cache in front of a database has two invalidation hops, and the local one has no way to hear about a write on another instance except by TTL or by a pub/sub invalidation channel that can itself drop messages.

What people get wrong

“Write-through is safe.” It is safe against a cache crash. It is not atomic across two systems, and its failure mode is a cache entry that disagrees with the database until its TTL expires.

“Write-back is just an optimisation.” It is a durability decision with a computable loss window. It is a fine decision for view counters, session state and telemetry; it is a resignation letter for payments.

“We update the cache on write to keep it fresh.” And you have introduced a race. Delete instead.

“TTL will fix any inconsistency.” It bounds it. Whether a bound of five minutes is acceptable is a product question, and it should be answered rather than assumed.

In production

Redis used as a look-aside cache with DEL on write is the overwhelming default, and it is write-around plus invalidation — usually the right starting point. Write-back appears in specific places: high-frequency counters flushed periodically, Kafka producer batching (linger.ms is literally a flush interval with an exposure window), and OS page-cache writeback, where dirty_expire_centisecs is the same dial under a different name.

The pattern worth internalising is that every one of those systems exposes the flush interval as configuration, because the loss window is a business decision that infrastructure cannot make for you.

The follow-up questions

“You cache user profiles. A user edits theirs. Walk me through the write.” — Database first, then delete the cache entry, and say what happens if the delete fails. If read-your-writes matters, mention replication lag too, because the read may land on a follower that has not seen the write either.

“What if the cache write succeeds and the database write fails?” — The reason to write the database first and treat the cache as derived.

“How much data does write-back lose?” — Flush interval times write rate. Give the number.

“Two concurrent writers, one cache. What can go wrong?” — Interleaved set-after-read leaving a permanently stale entry. Delete-on-write avoids it.

In an interview

The read path gets all the attention. The write path is where the data loss lives, and where the interesting follow-ups are.

  • durability
  • write-back
  • invalidation
  • crash consistency

Run these next

The rest of caching