R1.03.2 · Write-behind — dive 2
The durability window.
Between a write and its flush, the cache holds the only copy. That window is the price of the throughput — the flush interval sets its width, and durable storage sets whether a crash in it loses anything.
Grounding: echo_store/journal.ex (the SQLite WAL, replay/2, compact/1) · bcs.4.md (the measured price) — verbatim. Engine: Valkey.
The unflushed window
write-behind.md.txt · The Durability Trade-offThe trade is the inverse of write-through. Write-through writes the database on the request, so the source is always current and a cache loss costs nothing. Write-behind defers that write, so between a write and the next flush the database is behind and the cache holds the only copy of those changes. If the cache is lost in that window — a crash, a failover, an eviction — the unflushed writes are gone.
The window’s width is the flush interval. With a one-second interval and a steady stream, roughly one second of writes is at risk at any moment; with a five-second interval, five seconds is. Narrowing the interval narrows the window but shrinks the batches, which is exactly the throughput the pattern was bought for. The choice is a dial, not a default.
- durability window
- The span from a write landing in the cache to its flush. In it, the cache holds the only copy of that write.
- at-risk writes
- Everything written but not yet flushed. A crash in the window loses exactly these.
- flush interval
- The dial. Shorter narrows the window and the loss; longer batches more and risks more.
- RPO
- Recovery point objective — how much recent data a system may lose on a crash. Write-behind’s RPO is bounded by the window.
A second lever shrinks the window without touching the interval: flush on shutdown. A clean stop drains the buffer first, so a planned restart loses nothing — only an unclean crash hits the window.
Persistence bounds the loss
write-behind.md.txt · Mitigating Data LossThe flush interval bounds how many writes the window holds. Durable storage bounds whether a crash actually loses them. The cache can run purely in memory, snapshot to disk on a schedule (RDB), or append every write to a log (AOF). Each mode changes what survives a process restart, and so changes the real risk of the unflushed window. Pick a persistence mode to classify what a crash loses for the same buffered writes.
Two dials bound the risk: the flush interval sizes the window, and durable storage sets whether a crash in it actually loses anything.
The window on EchoStore — the lane that remembers
journal.ex:103 · :125-126 · bcs.4.mdEchoStore does not put an append log on the cache. The bus stays volatile by decision; durability for the
job lane’s obligations lives in a per-group SQLite WAL standing beside the bus
(journal.ex:125-126 — PRAGMA journal_mode=WAL, synchronous=NORMAL).
The outbox closes the window by construction: the intent is recorded before the bus hears it, and
replay/2 re-enqueues every intent not yet covered by the applied memory after a bus restart. The
bus already deduplicates a re-enqueued job id and newer-wins makes a re-applied version harmless, so replay
is at-least-once and harmless by comparison.
The committed record prices the memory exactly. The writer’s edge costs 143 us per
record-and-mark pair; the remembered-lane median is 524 us against the bare lane’s 148; and
after a staged bus loss, replay re-enqueued exactly thirty uncovered intents in seq order.
What the outbox costs, and what it buys
The price is committed, figure by figure, against the bare lane — from the BCS manuscript record:
record-and-mark pair 143 us ← the writer's edge remembered-lane median 524 us ← against the bare lane's 148 replay after a bus loss exactly 30 uncovered intents re-enqueued in seq order the trade 3.5 times the latency buys an outbox, a last word per name, and a replay that survives the bus
Deferring the source write opens a window where the cache holds the only copy. The flush interval sizes the window; durable storage bounds whether a crash in it loses anything.
The intent is recorded in a SQLite WAL before the bus hears it, so replay/2 rebuilds the
lane after a bus restart — 143 us per record-and-mark, 524 us end to end,
the outbox the cost buys.
Notes on Valkeythe journal’s SQLite runs at
synchronous=NORMAL, where the checkpoint is the only sync barrier; the trade is that a machine
power loss may trim the unsynced WAL tail — every process and bus crash this part stages is fully
covered. The plain-cache analogue is SET … PX with persistence configured
(valkey.io/commands/set).
Write-behind is honest only when the window is named and bounded: a known interval, a known persistence story, and a change that can afford the loss.
Recap — name the window, bound it
the window → the coalescingThe throughput of write-behind is paid for by a window where the cache holds the only copy of the unflushed writes. The flush interval sets the window’s width and the count at risk; durable storage — in-memory, RDB, AOF, or EchoStore’s outbox plus replay — sets whether a crash in the window loses them. Buffer only changes that can afford the loss, and the trade is sound. The next dive shows how to make a burst of writes to one key cost a single flush.
Next in R1.03: R1.03.3 · Coalescing writes — many updates to one key between flushes write the database once.
References
sources · relatedSources
- Valkey — SET —
SET … PXsets value and TTL atomically; the cache write whose loss the window measures. - Redis — Documentation — persistence: RDB snapshots, AOF, and what each survives a restart.
- Sanfilippo, S. — antirez weblog — the Redis creator on durability and the honest limits of a fast store.
- Valkey — RPOP — the flush drain whose interval sets the width of the window.
Related in this course
- R1.03.1 · The async bufferthe previous dive
- R1.03.3 · Coalescing writesthe next dive
- R1.03 · Write-behindthe module hub
- /echomq/cachethe EchoStore Journal replays the outbox, in depth