skip to content

Redis has a CLIENT TRACKING feature used with the RESP3 protocol. What problem does it solve, and how does an application learn that a value it has cached locally is no longer valid?

level: middleimportance: should knowfreq 38%

answer

  1. local in-process copy + server-sent invalidation
  2. HELLO 3 → RESP3 push messages
  3. CLIENT TRACKING on; 'invalidate' push with key list
  4. null key list = flush everything
  5. RESP2 fallback: REDIRECT to __redis__:invalidate

basics

~20 s

It lets the application keep a copy of hot values in its own process memory and have Redis tell it when they change. After CLIENT TRACKING ON over RESP3, the server records which keys the connection read and pushes an 'invalidate' message listing keys the client must drop when they are modified, expired, or evicted.

solid answer

~60 s

Even a cache hit costs a network round trip. **Client-side caching** puts the value in the application process itself, so hot reads cost nothing — but then the app needs to know when to drop it. `CLIENT TRACKING ON` makes Redis do the telling, which is why Redis calls it a *tracking* or assisted client-side cache. Mechanics: with **RESP3** (Redis 6.0+, negotiated with `HELLO 3`) the connection can carry out-of-band **push** messages. After `CLIENT TRACKING on`, the server remembers, in a global invalidation table, which keys each connection has read. When a key is modified, expires, or is evicted, the server pushes an `invalidate` message containing the key names to every client that read it; the client removes them from its local map. A null key list means "drop everything" (sent on FLUSHALL/FLUSHDB). On RESP2 you can still use it via `CLIENT TRACKING on REDIRECT <client-id>`, which delivers invalidations as Pub/Sub messages on `__redis__:invalidate` to a second connection that has subscribed. This is best-effort, not a distributed-coherence protocol: keep a local TTL and a size cap, and flush the local cache if the tracking connection drops.

code

text · 12 lines
text
HELLO 3
CLIENT TRACKING on
OK

GET config:v1:flags
"{\"beta\":true}"        # key is now tracked for this connection

# another client runs: SET config:v1:flags "{\"beta\":false}"

# this connection receives, out of band:
# push: "invalidate" -> 1) "config:v1:flags"
# -> application removes the key from its in-process map

go deeper

for a junior

Know that the app can keep values in its own memory and that CLIENT TRACKING makes Redis push 'invalidate' messages naming keys to drop.

for a middle

Explain RESP3 push versus the RESP2 REDIRECT fallback, which events trigger invalidation, and why a local TTL and size cap are still required.

for a senior

Add the server-side invalidation table, tracking-table-max-keys and spurious invalidations, reconnect flushing, and per-node enablement in cluster mode.

for a principal

Position it against alternatives (short local TTL, application Pub/Sub invalidation) and decide which data classes may tolerate a millisecond-scale invalidation window at all.

## The problem: a hit still costs a round trip A Redis GET is microseconds of server work, but the round trip is typically 0.1-0.5 ms inside a datacenter. For a value read thousands of times per second per process — a feature flag, a config blob, a permission set, a hot catalog entry — that adds up to real latency and real load on one Redis node. The obvious fix is to keep the value in the application's own memory. The hard part is invalidation: the process now holds a copy that nothing tells it to drop. The traditional workarounds are a short local TTL (accept bounded staleness), or a Pub/Sub channel your application publishes invalidations to (works, but every writer must remember to publish, and it is your code's job to get right). **CLIENT TRACKING** makes the server responsible for the telling. ## RESP3 and push messages RESP2, the classic Redis protocol, is strictly request/response on a connection: the server only speaks when spoken to. The single exception was Pub/Sub, which effectively takes the connection over. RESP3 (introduced in Redis 6.0, and requested by the client sending `HELLO 3`) adds a **push** message type: out-of-band data the server can deliver on a normal connection, interleaved with ordinary command replies, and distinguishable from them. Client-side-caching invalidation rides on that. This is why tracking is described as a RESP3 feature even though a RESP2 fallback exists. ## What the server remembers In the default tracking mode, the server maintains one global **invalidation table**: a mapping from key name to the set of client IDs that have recently read that key. It is intentionally approximate and memory-bounded by `tracking-table-max-keys` (default 1,000,000). When the table is full, Redis evicts entries and, to stay safe, **sends invalidation messages for the evicted keys** — clients may receive an invalidation for something that did not actually change. Spurious invalidations are harmless (an extra miss); missed ones would not be, so the design errs that way. Caching keys are remembered when a read command touches them. Invalidations fire when a key is **modified, expired, or evicted** — all three, because from the client's point of view all three mean "your copy is no longer what Redis has". ## The wire flow ``` HELLO 3 CLIENT TRACKING on GET config:v1:flags -> "{...}" # server now tracks this key for this client # ... application caches the value locally, serves it from memory ... # elsewhere: SET config:v1:flags "{...new...}" >2 # push message "invalidate" 1) "config:v1:flags" # client drops its local copy ``` A push with a **null** key array means flush the entire local cache; the server sends it on `FLUSHALL`/`FLUSHDB`. ## The RESP2 fallback: REDIRECT If your client library cannot speak RESP3, `CLIENT TRACKING on REDIRECT <client-id>` routes this connection's invalidation messages to a **different** connection — one that has subscribed to the special Pub/Sub channel `__redis__:invalidate`. You get the same information at the cost of managing two connections and their lifecycles. If the redirect target disconnects, tracking is broken; the server notifies the tracking client with a `tracking-redir-broken` push (RESP3) so it knows to flush and re-establish. ## NOLOOP and other options `CLIENT TRACKING on NOLOOP` suppresses invalidations for keys **this** client itself modified — useful when the writer already knows it invalidated its own copy and does not want the echo. There are also `BCAST` with `PREFIX`, and `OPTIN`/`OPTOUT` modes for controlling *which* keys get tracked; those are large enough topics on their own. `CLIENT TRACKINGINFO` (Redis 6.2) reports the current connection's mode, prefixes, and redirect target, which is the first thing to run when debugging. ## What it is not This is **best-effort invalidation, not coherence**. There is a real window between a key changing and the push arriving, during which the local copy is stale — bounded by network latency plus client processing, but not zero. There is no acknowledgement, no versioning, and no guarantee against loss if the connection breaks. So the local cache still needs the ordinary defenses: a **local TTL** as a backstop, a **bounded size** with local eviction (an unbounded in-process map is a memory leak), and a **flush on reconnect**, since anything that happened while the connection was down was never delivered. Treat tracking as an accelerator that shortens staleness from "the local TTL" to "a few milliseconds", not as a guarantee that removes the need for the TTL. ## Where it pays The wins are largest for values with a very high read-to-write ratio and a small working set: configuration, feature flags, entitlement lookups, small reference data. It pays poorly for large working sets (you will not fit them in process memory anyway) and for write-heavy keys, where the invalidation traffic can exceed the savings. In Redis Cluster, tracking is per connection to each node, so a client must enable it on every node connection it uses.

  • Which events cause Redis to send an invalidation message?
    Modification of a tracked key, its expiration, and its eviction under maxmemory — all three, because each means the client's local copy no longer matches the server. Redis also sends invalidations for keys it drops from its own tracking table when that table hits tracking-table-max-keys, so clients must tolerate spurious invalidations. A push carrying a null key list means flush the entire local cache and is sent on FLUSHALL or FLUSHDB.
  • Why is RESP3 relevant to this feature at all?
    RESP2 is strictly request/response, so the server has no way to speak on a connection unasked. RESP3, added in Redis 6.0 and negotiated with HELLO 3, introduces a push message type that can be interleaved with normal replies and told apart from them, which is what carries invalidations. On RESP2 you must fall back to CLIENT TRACKING with REDIRECT, delivering the same messages over Pub/Sub on a second connection.
  • Do you still need a local TTL if the server sends invalidations?
    Yes. Invalidation is best-effort: there is a delivery window during which the local copy is stale, messages are lost if the connection drops, and nothing acknowledges them. A local TTL plus a bounded, evicting local cache and a flush on reconnect keep the failure mode to bounded staleness rather than indefinite staleness or unbounded memory growth.

It is a subscription to errata: you keep your own printed copy of the page, and the publisher mails you a note naming exactly which pages are now wrong.

saying these in an interview costs you the question

  • Describing tracking as a strong consistency or coherence protocol rather than best-effort invalidation.
  • Assuming it works on RESP2 without the REDIRECT fallback and a subscribed second connection.
  • Keeping an unbounded in-process map with no local TTL or size limit because 'Redis will tell us'.
  • Thinking only writes trigger invalidations, forgetting expiration and eviction.
  • Not flushing the local cache after the tracking connection reconnects.

context