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

R1.02 · Caching — module hub

Write-through.

Maintain cache-database consistency by synchronously writing to both the cache and the database before returning success — so reads always hit the cache with fresh data.

Grounding: echo/apps/echo_cache · EchoCache.Table.put · content/bcs4.1.md — every figure verbatim from a committed record; the engine is Valkey.

One write, both layers. Select a stage of the write to read what it does and whether the call has returned.
Table.putone GenServer.call SET PX insert L2 · Valkey rowversion <> value, PX L1 · ETS tableinsert(id, value, ver) both land before the call returns return · :ok later readfetch → :hit
readout · Select a stage. The L2 row and the L1 table are both written in one synchronous call; the read after it is fresh by construction.

Static verdicts — L2 SET PX → value + TTL atomically (the larger latency term) · L1 insert → sub-microsecond, same call · return → only after both land · later read → served fresh from L1 (762 ns).

§1

How it works

write-through.md.txt · How It Works

When a write occurs, three steps run in order. The application writes the data to the cache. The application writes the same data to the database. Only after both writes succeed does the operation complete. The cache and the database are always updated together, which keeps them consistent with each other.

A write-through cache treats the two stores as one write target. When a value changes, the writer sets the cache with SET … PX and the database with an update, and the write does not return until both are done. Because the cache was updated on the same path as the source, a read that follows serves the cached value and matches the database. This is the trade against cache-aside, which fills the cache lazily on a read miss and drops it on a write: a cheaper, single-store write, but a window where the cache is stale or empty.

The cache and the database move together on the write. A read after the write serves a value that is, by construction, never behind the source.

§2

The commands the write uses

write-through.md.txt · Redis Commands Used

A write-through operation is a single cache write followed immediately by the corresponding database write, and the application returns success only after both complete. The cache half is one command: SET … PX sets the value and the millisecond TTL together — atomically.

SET ecc:{instruments}:AST0NgWEfAEJfs "<version><value>" PX 300000   # cache: value + TTL, atomically
UPDATE instruments ... WHERE id = ...                              # the corresponding database write
# return success only after both acknowledge

The key is the cache’s own form ecc:{<table>}:<id> (EchoCache.Keyspace.key/2): a fresh prefix beside emq:, with the table name hash-tagged in braces so every key of one cache lands in one cluster slot. The value is framed version <> value — a 14-byte branded version prefix in front of the bytes — so a later coherence message can compare newer-wins. The cache half of a write-through write is one command; the dual-write dive takes that command apart.

Notes on ValkeySET key value PX <ms> sets the value and the TTL in one atomic command. A separate EXPIRE after SET leaves an immortal key if the process dies between the two — valkey.io/commands/set.

§3

Advantages and disadvantages

write-through.md.txt · Advantages / Disadvantages

Advantages.

  • Strong consistency — the cache always reflects the latest data. There is no window where the cache holds stale information, because the cache write is on the same path as the database write.
  • Fast reads after writes — data written is immediately available for the next read. The L1 ETS hit it serves is 40 times cheaper than the L2 round trip (content/bcs4.1.md).
  • Simpler cache invalidation — because writes go through the cache, there is no need to explicitly invalidate keys after a database update.

Disadvantages.

  • Increased write latency — the write is not complete until both the cache and the database acknowledge, so the total latency is at least the sum. The latency dive measures this against write-behind.
  • Cache pollution — data that is written but rarely read still occupies cache memory, and can evict more valuable, frequently-read data.
  • Failure complexity — if the database write fails after the cache write succeeds, the two stores disagree. The application has to handle partial failures carefully.
§4

Handling partial failures

write-through.md.txt · Handling Partial Failures

The safest approach is to write to the database first. Write to the database. If that succeeds, write to the cache. If the cache write fails, report it rather than reporting success with a stale cache. This way the database — the source of truth — is always correct, and a later cache miss reads fresh data from the database. The latency dive walks each failure path and the state it leaves.

Write the database first, and a partial failure can never leave the cache ahead of the source of truth. The worst case is a cache miss, not a wrong answer.

§5

When to use — and when to avoid

write-through.md.txt · When to Use / Avoid

Use write-through when strong consistency between cache and database is required, write-then-immediately-read patterns are common, the write-latency overhead is acceptable, and the dataset is relatively small so cache pollution is less of a concern.

Avoid it when write latency is critical, most data is written but rarely read, or the system needs to tolerate cache failures without affecting write operations. A write-heavy, stale-tolerant workload reaches for write-behind instead — the next module.

Size the choice for a workload. Select a workload to read which strategy fits and what a write-through write costs in a fixed model.
readout · Select a workload. Write-through fits one that reads its own writes and cannot tolerate a stale window.

Fixed cost model — one cache write ≈ 1 unit, one database write ≈ 8 units; a write-through write pays both on the hot path.

§6

The three dives

the write → the guarantee → the cost

Each dive takes one part of write-through, in the arc the write → the guarantee → the cost: the dual write that touches both layers, the read-after-write freshness it buys, and the latency every write pays for it. Read them in order — the synchronous dual write starts.

the application · the put clause, against live Valkey

SET PX then insert — both layers, one call

The dual write is not an aspiration — it is the handle_call({:put, …}) clause in the as-built table: set the L2 row with the value and TTL atomically, then insert into L1, on the one call.

echo/apps/echo_cache/lib/echo_cache/table.ex · handle_call({:put, …})as-built
l2 = Keyspace.key(state.table, id)            ← "ecc:{instruments}:AST0NgWEfAEJfs"
{:ok, "OK"} =
  Connector.command(state.conn, [
    "SET", l2, version <> value, "PX",      ← L2 Valkey: value + TTL, atomically
    Integer.to_string(state.spec.ttl_ms)
  ])
insert(state, id, value, version)             ← L1 ETS, on the same call
{:reply, :ok, state}                          ← returns only after both land
write-through

Write the cache and the source in one synchronous step, so a read after a write is always fresh — at the cost of a second write on every change.

its EchoCache application

EchoCache.Table.put runs SET … PX to the L2 Valkey row then insert into the L1 ETS table, both inside one synchronous GenServer.call; reads are always fresh.

Three dives, one pattern: the synchronous write that touches both layers, the read-after-write freshness it guarantees, and the latency it costs to keep that promise.

§7

References

sources · related

Sources

  • Valkey — SET — SET key value PX <ms> sets value and TTL atomically; the L2 write-path command.
  • Valkey — Topics — the engine EchoCache’s L2 layer is written against; the live line is Valkey.
  • Redis — SET — the string write-path command and its EX/PX expiry options.
  • Redis — Documentation — the data structures and commands the cache write is built from.

Related in this course