Skip to content
About
Main content

Write routing, and why you delete rather than update

Why is write-back a durability decision?

The two dimensions

Dimension A: write routing. Where does the write go, and who pays its latency?

Write-through writes to the and to the store, both synchronously, before it acknowledges the write. The path it covers is never stale. The write becomes max(cache, store). It also carries a cost that nobody lists:

Admission means deciding which items enter the cache. Suppose a workload writes a large key space and reads a small part of it. fills the cache with entries nobody will read. That destroys the that What makes data cacheable told you to protect.

Write-around writes only to the store, and drops (or invalidates) the cache entry. Admission then follows read locality, which is what you want. The price is that the first read after every write is a miss. A workload that reads right after it writes gets a zero hit rate on exactly the keys people are asking about.

The choice between them is a measurement, not a matter of taste:

  • High (session state, a shopping cart, a user's own profile): use write-through. The write is a reliable predictor of an imminent read.
  • Low (event ingestion, log writes, bulk loads): use . Write locality and read locality are different distributions, and mixing them costs you both.

Write-back (also called write-behind) is not a freshness strategy at all, and filing it under freshness is a category error. A cache acknowledges the write as soon as the cache has it, and flushes to the store later. That moves the source of truth into the cache. Three consequences follow, and each one belongs to a different field:

  1. A loss window equal to the flush interval. If the cache dies before it flushes, those writes are gone. The cache has become a durability component.
  2. Eviction is no longer free. A dirty entry (changed but not yet flushed) must be flushed before it can be dropped, so eviction now depends on the store being available. When the store slows down, evictions stall, dirty entries pile up and writes stall. This has the same shape as the debt that builds up in an LSM-tree's compaction (the background merge of files in a write-optimized database), and the same shape as a garbage-collection death spiral.
  3. Ordered writes need a log. If the store requires writes in a certain order, the cache must keep a log of them. At that point you have built a write-ahead log inside your cache.

So write-back re-creates all of durability inside the cache: durability of the dirty set, ordered flush, and recovery. Every write-back cache that matters has all three. Examples are the operating system's page cache, an SSD's capacitor-backed DRAM buffer, and a database buffer pool. A write-back cache with none of the three is a data-loss mechanism with a performance story.

Its real benefit is . If a key is written w times per second and the flush interval is T_flush, then writes collapse into one flush. This is the rule that a fixed cost per operation gives a natural batch size, with the flush as the fixed cost.

The cell that is easy to overlook: write-invalidate versus write-update

On a write, should we delete the cache entry, or overwrite it with the new value? The second sounds better: the next read is a hit. Work through the race to see why it is wrong.

Two writers, W1 and W2, change the same key.

W1: store.put(k, v1)
W2: store.put(k, v2)          # the store now holds v2 (W2 was last)
W2: cache.put(k, v2)
W1: cache.put(k, v1)          # W1 was slow: the cache now holds v1 FOREVER

The cache holds v1 permanently while the store holds v2. The reason is simple: nothing orders the cache writes with respect to the store writes. The cache and the store have no shared atomic operation that covers both.

Now use delete:

W1: store.put(k, v1);  W2: store.put(k, v2)
W2: cache.delete(k)
W1: cache.delete(k)           # the cache is empty; the next read refills from the store (v2)

Delete is idempotent (doing it twice is the same as doing it once) and self-healing. Update is a lost-update bug that waits for a bad coincidence of timing. The rule is "delete, don't update." It is a practical result of a deeper principle: when two things share no atomic primitive, one of them (the store commit) must be the designated write, and the other must be made to forget, not to agree.

Delete does not close every window. One race remains, and it is the one that actually hurts in production:

The delete that came too early

( is the common pattern where the application checks the cache, and on a miss reads the store and fills the cache itself.) The only things that bound this race are a backstop, versioned keys, or a or compare-and-set (CAS) token protocol, where the filler may only write if the entry's token is unchanged. This is why a TTL is not optional, even in a system with perfect .