jonnify·redis /redis-patterns/caching/client-side-caching
Redis Patterns / R1 · Caching / Client-side caching

R1.04 · Caching — module hub

Server-assisted client-side caching.

Eliminate network round-trips for frequently accessed keys by caching values in application memory, with the engine automatically sending invalidation messages when data changes.

Grounding: echo_store/coherence.ex · echo_store/table.ex · echo_wire/script.ex · bcs.4.md — every figure verbatim from a committed record.

Near-cache hit ratio. Round-trips removed over 10,000 reads — move the dial and read the saving.
90%
10,000 reads memory · 9,000 Valkey · 1,000 no round-trip
readout · Move the dial.
§1

The architecture

source · echo_store/table.ex

Client-side caching creates a multi-tier cache. L1 (local memory) is nanosecond access, held by the application; L2 (Valkey) is microsecond to millisecond access; L3 (the database) is millisecond access. The key innovation is that the server records which keys each client holds and sends an invalidation message when those keys change.

The payoff is the round-trip a cache-aside hit still pays. With cache-aside the data is in Valkey, one network hop away; a hit is fast but still a request and a response across a socket. For a key read thousands of times a second and changed rarely — an emoji set, a round secret, a config blob — that hop is pure overhead. A near cache removes it: a repeated read of an unchanged key is a memory lookup with no network at all, and the value stays correct because the server pushes a message the moment it goes stale. This is exactly the shape of EchoStore (echo/apps/echo_store): an L1 ETS table in the caller's process over an L2 Valkey row, with coherence carried by a server-side push.

The three tiers. Select a tier to read its cost and who owns it.
L1 · local memory (ETS)nanoseconds · the caller's process L2 · Valkeymicroseconds · ecc:{cm_emojisets}:<id> L3 · the databasemilliseconds · the system of record
readout · Select a tier. L1 is the near cache the server keeps honest; L2 is the shared store; L3 is the loader behind both.

A near-cache hit costs a memory read and zero network. The only cost is correctness — and the server pays that with an invalidation push.

§2

Tracking modes

source · CLIENT TRACKING

Default tracking. The server remembers which keys each client has requested. After CLIENT TRACKING ON, every GET causes the server to record that this client accessed that key; if another client modifies the key, an invalidation message is pushed to the original client. The trade-off is that the server must maintain a table mapping keys to client IDs, consuming memory proportional to the number of tracked keys.

Broadcasting mode. Instead of recording individual key accesses, clients subscribe to key prefixes; the server broadcasts invalidation messages to all subscribers when any matching key changes. A client running CLIENT TRACKING ON BCAST PREFIX user: PREFIX session: receives invalidations for any key starting with user: or session:. The trade-off reverses: a client may receive invalidations for keys it does not have cached locally, but server memory stays minimal because it tracks prefix subscriptions, not individual accesses. EchoStore rides the broadcast shape over plain pub/sub: a table subscribes to one channel, ecc:{<table>}:coh, and the writer publishes a message naming the changed key.

How invalidation works. When tracking is enabled and a key changes, four steps follow. The server checks which clients hold the key; it sends an invalidation message naming the key; the client removes the key from its local cache; and the next access fetches fresh data. This keeps local caches synchronized without polling. The message names the key and carries no value — it reports only that the held copy is no longer trustworthy. In EchoStore the message carries one more thing than a bare CLIENT TRACKING invalidation does — the writer's mint-time version — so a late stale message can never erase a newer row.

CLIENT TRACKING ON                          # enable tracking for the connection
CLIENT TRACKING ON BCAST PREFIX user:       # broadcast mode with a prefix
CLIENT TRACKING ON REDIRECT 123             # redirect invalidations to another connection
CLIENT TRACKING ON NOLOOP                   # do not invalidate the writer's own copy
CLIENT TRACKING OFF                         # disable tracking

Default tracking is exact and table-backed; broadcast trades a per-key table for prefix subscriptions — cheaper memory, coarser notification. The first dive measures that trade.

§3

When to use it

source · considerations

This pattern benefits ultra-high-throughput applications where network RTT matters, frequently accessed "hot" keys, read-heavy workloads with relatively stable data, and applications already holding local caches that need invalidation.

Three considerations bound it. Memory management — the application must cap its local cache size, evicting when memory is constrained. Complexity — handling invalidation messages requires dedicated connection management or async processing. Not for volatile data — if data changes constantly, invalidation traffic may exceed the benefit of caching. RESP3 carries push notifications natively, making invalidation handling cleaner; EchoStore requires it for the broadcast lane — the table opens a RESP3 connection precisely so a push and a reply can share one wire.

Notes on ValkeyServer-assisted client-side caching pushes an unversioned, orderless invalidation — it names a key and nothing more. That is the gap EchoStore's mint-time version closes: its 29-byte message carries the version, so newer-wins resolves a late stale message by comparison — valkey.io/topics/client-side-caching.

§4

The broadcast lane in EchoStore

echo_store/coherence.ex · bcs.4

The near cache codemojex puts in front of its emoji set is an EchoStore table: an L1 ETS row in the caller's process over an L2 Valkey row keyed ecc:{cm_emojisets}:<id>. Coherence is carried by a server push, not by polling. On start the table opens a RESP3 connection and subscribes to its coherence channel; when a writer changes a row, it publishes the change on that channel, and every subscribing table applies it newer-wins in its owner.

# echo/apps/echo_store/lib/echo_store/coherence.ex
def channel(table), do: "ecc:{" <> table <> "}:coh"
def payload(<<_::binary-14>> = id, <<_::binary-14>> = version), do: id <> ":" <> version

def broadcast(conn, table, id, version) do
  Connector.command(conn, ["PUBLISH", channel(table), payload(id, version)])
end
the application · the broadcast lane, priced

Two lanes, one 29-byte message

The coherence message is the same twenty-nine bytes on either lane — a cached name, a colon, the writer's mint-time version. The broadcast lane is fast and lossy; the durable job lane survives a crash. The committed record prices both:

bcs.4.md · the coherence lanes, measuredfrozen
coherence message            twenty-nine bytes   ← name + ":" + mint-time version
broadcast lane (PUBLISH)     72 us   ← fire-and-forget, at-most-once
job lane (EchoMQ)            148 us         ← at-least-once, crash-surviving
the guarantee costs          2.1 times the latency
client-side caching

The application caches a value locally and reads it from memory. The server tracks the key; on a change it pushes an invalidation, and the local copy is dropped.

its EchoStore application

An L1 ETS row over an L2 Valkey row; a RESP3 subscription on ecc:{<table>}:coh. Coherence.broadcast/4 PUBLISHes the 29-byte message; the table drops L1 newer-wins.

Cache the hot and stable; leave the volatile in Valkey. The benefit is the round-trip removed; the cost is the memory the near cache holds and the invalidation traffic it must process.

§5

The three dives

track → invalidate → the same shape

The module takes the pattern in three steps, in the arc track → invalidate → the same shape elsewhere: how the server records what each client holds, how it notifies a holder when a key changes, and a real EchoMQ script cache that follows the identical cache-then-invalidate idea. Read them in order — CLIENT TRACKING starts.

The third dive draws a parallel only. The script cache uses the same cache-then-invalidate idea as CLIENT TRACKING; the queue protocol itself — the full Lua bundle and the version fence — belongs to the dedicated EchoMQ course, which this module links forward to rather than teaches.

§6

References

sources · related

Sources

Related in this course