jonnify·redis /redis-patterns/caching/write-through/latency-cost

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.

Set the latencies. Move the two sliders to read the write-through total against write-behind’s deferred database write.
2 ms
16 ms
write-through 18 ms write-behind 3 ms

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).

§1

Every write pays for both layers

write-through.md.txt · Disadvantages

EchoStore.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 SET round-trip + the L1 insert. 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.
§2

When one layer fails

write-through.md.txt · Handling Partial Failures

Writing 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.

Pick a failure. Select which layer fails to read the state it leaves and how the put clause reports it.
readout · Select a failure. The source is written first, so its failure stops the write before the cache changes.

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.

§3

The cost on EchoStore

table.ex · the synchronous call · verbatim

On 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.

echo/apps/echo_store/lib/echo_store/table.ex · put/4 and its call · verbatimas-built
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.

the application · the deferred-write alternative

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.

the cost

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.

its EchoStore application

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.

§4

Recap — the price of the guarantee

the synchronous call

Write-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.

§5

References

sources · related

Sources

  • 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 PX expiry.
  • Redis — Documentation — the persistence and durability options behind the cost and durability trade.

Related in this course