ReadThrough vs WriteThrough Cache
Read-through and write-through are not two choices for the same decision — read-through governs what happens on a cache miss (the cache library, not your application, loads the value from the backing store and populates itself), while write-through governs what happens on a write (the cache library writes through to the store synchronously and only then acknowledges). One is a read policy, the other a write policy; they sit on orthogonal axes, and a real caching layer picks one of each. Comparing them head-to-head is like comparing "how you get groceries in" against "how you restock the shelf" — related, but not rivals.
The genuine rivalries are within each axis. On reads, read-through competes with cache-aside (lazy loading). On writes, write-through competes with write-back and write-around. That is where the judgment lives, and where consistency and durability are actually traded.
The two axes, and who owns the logic
The defining question for a read policy is where the miss-handling code lives. With cache-aside, your application owns it: on a miss, your code queries the DB and writes the value back into the cache. With read-through, the cache sits inline as a provider — your code only ever calls cache.get(key), and the cache itself fetches-and-populates on a miss. Same physical data flow (App → Cache → DB → Cache → App); the difference is who wrote the code and therefore who owns the failure modes.
The write axis has three options, distinguished by when the store is updated relative to the ack:
- Write-through — write hits cache, cache writes the store synchronously, then acks. Store is always current; every write pays store latency.
- Write-back (write-behind) — write hits cache, cache acks immediately, and the store is updated asynchronously (batched/coalesced). Fast writes and write coalescing, at the cost of a durability window.
- Write-around — the write goes straight to the store and skips the cache entirely; the entry is populated only later, on a read miss. Avoids polluting the cache with write-once data.
Read-through vs cache-aside — the code tells the story
Cache-aside, written explicitly in your service:
def get_product(pid):
key = f"product:{pid}"
val = cache.get(key) # 1. look in cache
if val is not None:
return val # hit
val = db.query_product(pid) # 2. miss: app loads from DB
cache.set(key, val, ttl=60) # 3. app populates cache
return valRead-through deletes steps 2–3 from your code and moves them into the cache provider (a CacheLoader in Caffeine/Guava, a DataLoader/loader function in a read-through client). Your service just calls cache.get(key) and the loader runs on a miss.
Why the naive write path is wrong. A common bug when pairing a read cache with writes is to update the cache after updating the DB: db.update(...) ; cache.set(key, newval). Two concurrent writers can interleave so the cache ends up holding the older value permanently. The correct cache-aside write is update the DB, then invalidate (delete) the key — db.update(...) ; cache.delete(key) — so the next read re-loads fresh. Facebook's memcache team documented exactly this class of stale-set race. Write-through sidesteps it because the cache and store are updated in one ordered operation.
Invalidation shrinks the race window but does not close it. A reader that loaded the old DB value just before your write can still set it just after your delete, re-installing stale data until TTL. The interleaving: (1) reader misses on key K and reads the DB → old value; (2) writer updates the DB and deletes K; (3) the slow reader now executes cache.set(K, old) — the cache holds the old value while the DB holds the new one, and no further invalidation is coming. Facebook closes this with leases (the same NSDI 2013 memcache paper): a cache miss hands the reader a lease token, a delete invalidates outstanding tokens, and the cache rejects any set whose token has been invalidated — turning last-write-wins into first-writer-since-invalidation-wins. If your cache has no leases, your backstop is the TTL: choose it as the maximum staleness you can tolerate from exactly this race.
Worked trace: read-through + write-through on product:42
Redis in front of Postgres. Assume: cache read 0.5 ms, cache write 0.5 ms, DB read 8 ms, DB write 10 ms. TTL on cached entries = 60 s. A seller changes the price of product 42 from 799 to 899.
| Step | Operation | What happens | Latency | State after |
|---|---|---|---|---|
| 1 | GET price(42) | Cache miss. Read-through loader queries DB (799), populates key with TTL 60s. | 0.5 + 8 = 8.5 ms | cache: 799 (ttl 60s); db: 799 |
| 2 | GET price(42) | Cache hit. Served from Redis. | 0.5 ms | cache: 799; db: 799 |
| 3 | SET price(42)=899 | Write-through: cache updated, then DB written synchronously, then ack. | 0.5 + 10 = 10.5 ms | cache: 899; db: 899 |
| 4 | GET price(42) | Cache hit — already fresh, no miss, no DB touch. | 0.5 ms | cache: 899; db: 899 |
Now swap the write policy to write-back at step 3: the ack returns in 0.5 ms (10 ms saved per write), but the DB still holds 799 until the async flush fires ~200 ms later. Any process reading the DB directly in that window — a replica, an analytics job, a recovery after a cache crash — sees the stale 799. That gap is the durability/consistency price of write-back.
What happens on eviction and TTL
Every entry leaves the cache one of three ways, and the policy pairing determines whether that is safe:
- TTL expiry — the entry is dropped after its time-to-live. Harmless with read-through: the next read simply misses and re-loads. This is the main tool for bounding staleness when writes bypass the cache.
- Capacity eviction (LRU/LFU/etc.) — under memory pressure the cache discards cold keys. With read-through the value is regenerated on the next miss. With write-back this is a hazard: if a dirty entry (written to cache, not yet flushed) is evicted before its flush, you either lose the write or the cache must flush-on-evict, which turns a cheap eviction into a synchronous DB write and can stall.
- Explicit invalidation — a
delete(key)after an out-of-band DB update. Required whenever writes can reach the store without going through the cache (write-around, or another service writing the DB).
Key consequence: read-through + write-through is clean under eviction because the cache never holds un-persisted state. Write-back makes eviction a correctness question, not just a performance one.
Pitfalls
- Thundering herd on a hot miss. When a popular key expires or is evicted under read-through, thousands of concurrent requests all miss at once and stampede the DB with the same query. Mitigate with per-key request coalescing (single-flight /
loadIfAbsentlocking) or probabilistic early re-computation. - Treating write-through as durable when the DB write is fire-and-forget. Write-through only guarantees consistency if the store write is synchronous and confirmed before the ack. Some "write-through" client configs actually queue the DB write — that is write-back wearing the wrong label, with a silent durability window.
- Write-back dirty-entry loss. A cache node crash (or an eviction of an unflushed entry) loses every write still in the buffer. Only acceptable where the data is reconstructable or loss-tolerant (counters, view tallies, sessions) — never for money.
- Stale-set race on the read path. Covered above: update-then-set instead of update-then-invalidate leaves a permanently stale key. Prefer invalidation, or write-through so the cache is updated in-order.
- Write-heavy keys polluting a read cache. If you write-through data that is rarely read afterwards, you fill the cache with cold entries and evict useful ones. That is exactly the case write-around exists for.
- Cache down = writes down (write-through). If the cache is inline on the write path and it is unavailable, writes fail unless you have a bypass-to-store fallback. Availability now depends on the cache too.
When to use it / when NOT to — and the alternatives
Read axis: read-through vs cache-aside
Choose read-through when you want a single, uniform cache access path and are willing to centralize loader logic (and can rely on the cache library's coalescing/TTL). You gain simpler call sites and one place to fix miss behavior. It costs flexibility: the loader must be generic, you cannot easily fetch multiple keys in one DB round-trip, and you inherit the library's semantics. Prefer cache-aside when reads need custom logic (batching, joins, partial fields, per-request DB routing) or when you want the cache to be strictly optional so an app can still serve on a cache outage. Signal to reach for cache-aside: "the miss path is more than one query."
Write axis: write-through vs write-back vs write-around
Choose write-through when read-after-write consistency matters and reads immediately follow writes (user edits their own profile, price update that must show instantly), and write volume is modest enough to absorb store latency on every write. You gain a cache that is never stale and safe under eviction; you pay store latency and store load on every write, and couple write availability to the cache.
Prefer write-back when writes are the bottleneck and are bursty or repeatedly overwrite the same key (metrics, counters, leaderboards, session heartbeats). You gain low write latency and write coalescing (100 increments → 1 DB write); you pay a durability window and eviction-correctness complexity. Size that window before choosing it: if the buffer flushes every 50 ms and you are taking 20,000 writes/s, a cache-node crash loses up to 50 ms × 20,000/s = ~1,000 acknowledged writes — invisible for view counters, catastrophic for payments. That product (flush interval × write rate) is the concrete crossover that decides whether write-back is admissible. Never for data you cannot afford to lose.
Prefer write-around for write-once / read-rarely data (audit logs, event ingestion) where caching the write would only pollute the cache. You gain a clean cache; you pay a guaranteed miss on the first read of any freshly-written item.
The common default for a general read-heavy service is cache-aside reads + write-around (invalidate on write), because it keeps the cache optional and never traps un-persisted state. Read-through + write-through is the choice when you want the cache to be the authoritative, always-fresh access layer and can accept the write latency and the cache being on the critical path.
Takeaways
- Read-through and write-through are orthogonal: a read policy and a write policy. Pick one from each axis; they are typically paired, not compared.
- The real read-side rival is cache-aside (who owns the miss code); the real write-side rivals are write-back (fast, durability window) and write-around (skip cache on write).
- Write-through's virtue is that the cache never holds un-persisted state, so it is consistent and safe under eviction — at the cost of store latency on every write. Write-back inverts every one of those.
- TTL and eviction are part of the design, not an afterthought: read-through regenerates cleanly on eviction; write-back turns eviction into a correctness problem.
Sources: AWS Database Caching Strategies Using Redis
whitepaper and Amazon ElastiCache docs (lazy loading / write-through / TTL); Nishtala et al., Scaling Memcache at Facebook
, NSDI 2013 (stale-set races, invalidation, thundering herd); Martin Kleppmann, Designing Data-Intensive Applications (durability and write-behind trade-offs); Caffeine/Guava CacheLoader documentation for read-through loaders. Re-authored and deepened for this guide to correct the orthogonality error and add cache-aside, write-back, write-around, eviction/TTL behavior, and the durability trade-off.
🤖 Don't fully get this? Learn it with Claude
Stuck on ReadThrough vs WriteThrough Cache? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **ReadThrough vs WriteThrough Cache** (System Design) and want to truly understand it. Explain ReadThrough vs WriteThrough Cache from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **ReadThrough vs WriteThrough Cache** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **ReadThrough vs WriteThrough Cache** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **ReadThrough vs WriteThrough Cache** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.