R1.02.3 · Write-through — dive 3
The latency cost.
The freshness guarantee is not free: every write pays for the L2 round-trip on the hot path, and either layer can fail.
Grounding: the synchronous GenServer.call({:put, …}, 10_000) (table.ex) + bcs.4.md (762 ns vs 31 us) — the engine is Valkey.
The cache term is one ETS write (sub-microsecond) plus the L2 SET
round-trip — an L1 hit is 762 ns against a 31 us L2 GET
(40 times cheaper, bcs.4.md).
Every write pays for both layers
write-through.md.txt · DisadvantagesEchoStore.Table.put is a synchronous
GenServer.call(name, {:put, id, value, version}, 10_000): the caller blocks until the owner has
set the L2 Valkey row and inserted into the L1 ETS table. The L1 insert is cheap — an ETS write is
sub-microsecond — but the L2 SET is a network round-trip, the larger term, and write-through
puts it on every write. The committed figures size the gap: an L1 hit is 762 ns, an L2
GET is 31 us — the L1 hit is 40 times cheaper than the round trip
(bcs.4.md). Write-through pays that L2 cost on the write path rather than deferring it.
This is the trade write-behind makes differently. Write-behind writes the L1 cache, records the change locally, and returns — so its hot-path latency is the cache write plus a small local record, and the database catches up afterwards over the bus. Write-behind is faster on the write and risks losing the buffered write if the process dies before it drains; write-through is slower on the write and never loses the source write, because it does not return until the source has it.
- hot path
- The latency the caller waits on. Write-through puts the L2 round-trip on it; write-behind puts only the L1 write plus a local record.
- write latency
- For write-through, the L2
SETround-trip + the L1insert. For write-behind, the L1 write + a small local record. - durability
- Write-through never loses the source write — it returns only after the source has it. Write-behind can lose a buffered write on a crash.
- trade
- Write-through buys freshness and durability with latency; write-behind buys latency with a window of risk.
When one layer fails
write-through.md.txt · Handling Partial FailuresWriting two layers in sequence means there are two ways to fail, and the order determines what state is left behind. With the source written first and the cache second, a source failure stops the write before the cache changes, and a cache failure after a source success is reported rather than hidden behind a stale value. Pick a failure to see the state it leaves and how it is reported.
Static verdicts — source fails → both unchanged, the write is failed ·
L2 fails → the strict {:ok, "OK"} = … match fails the call, no stale success ·
both succeed → match holds, L1 insert runs, reply :ok.
Writing the source first bounds the damage: a source failure never leaves the cache ahead of the
source, and the strict {:ok, "OK"} = … match means a non-OK L2 reply fails the call, not a
stale success.
The cost on EchoStore
table.ex · the synchronous call · verbatimOn EchoStore’s write path, EchoStore.Table.put sets the L2 Valkey row, inserts into the
L1 ETS table, and returns once both are done — all inside one GenServer.call. A caller that
updates an emoji-set row waits for the L2 round-trip, and reads the fresh value back immediately. If the L2
SET does not answer {:ok, "OK"}, the match fails the call rather than reporting
success with a stale cache.
def put(name, id, value, <<_::binary-14>> = version) when is_binary(value) do # ... gate the kind ... GenServer.call(name, {:put, id, value, version}, 10_000) ← synchronous: the caller waits end # handle_call({:put, …}): {:ok, "OK"} = Connector.command(state.conn, ["SET", l2, version <> value, "PX", ttl]) ← the L2 round-trip; a non-OK reply fails the call insert(state, id, value, version) ← L1 insert, after the SET landed # write latency = L2 SET round-trip + L1 insert (both on the hot path)
Notes on ValkeyThe L2 SET is one round-trip to the engine;
the committed connector gate measured the round trip against live Valkey, where an L1 hit is
762 ns and an L2 GET is 31 us —
valkey.io/topics.
Where the write-behind write goes instead
The latency-cheaper alternative does not pay the L2 round-trip on the hot path: it records the change
locally in EchoStore’s Journal outbox and lets the apply drain at-least-once. The near-cache
and its write-behind outbox are taught in depth at /echomq/cache, and the cache
itself is the EchoStore manuscript, /bcs Part IV.
Pay for both writes on the hot path, in a fixed order, and report a failure rather than hide it — buying read-after-write freshness and durability with write latency.
The synchronous GenServer.call({:put, …}, 10_000) blocks for the L2 round-trip; the
strict {:ok, "OK"} = … match makes a non-OK reply a failed call, never a silent stale
value.
Write-through trades write latency and a second point of failure for the consistency guarantee. The write-heavy, stale-tolerant alternative is write-behind — the next module.
Recap — the price of the guarantee
the synchronous callWrite-through’s freshness costs the L2 round-trip on the hot path: the write latency is the L2
SET plus the L1 insert, both inside one synchronous call. Writing the source first
bounds the failure modes — the cache is never left ahead of the source, and a non-OK L2 reply fails the
call rather than hiding behind a stale value. Where write throughput matters more than a few milliseconds of
freshness, write-behind defers the database write instead. That is the next module.
References
sources · relatedSources
- Valkey — SET — the L2 write whose round-trip is added to every write-through write.
- Valkey — Topics — the engine the L2 layer runs on; the live line is Valkey, the round-trip the figures measured.
- Redis — SET — the string write-path command and its
PXexpiry. - Redis — Documentation — the persistence and durability options behind the cost and durability trade.
Related in this course
- R1.02 · Write-throughthe module hub
- R1.02.2 · The consistency guaranteethe previous dive
- R1.02.1 · The synchronous dual writethe write that costs this latency
- R1 · Cachingthe chapter; R1.03 Write-behind is the cheaper alternative
- /echomq/cachethe EchoStore write path and its deferral, in depth