Skip to content

Client-side caching

A cached read enables client-side caching for one call. The first read fetches the result from the server and caches it locally. Later reads of the same command use that local copy until the server invalidates it or the TTL expires.

Caching is opt-in per call. An ordinary get always goes to the server; only cached consults the local cache.

scala
client.set("cached:key", "v1")
// first fetches and caches; second is a local hit
val v1 = client.cached(Commands.get[String, String]("cached:key"), 1.minute)
val v2 = client.cached(Commands.get[String, String]("cached:key"), 1.minute)
scala
for {
  _  <- client.set("cached:key", "v1")
  v1 <- client.cached(Commands.get[String, String]("cached:key"), 1.minute)
  v2 <- client.cached(Commands.get[String, String]("cached:key"), 1.minute)
} yield (v1, v2)

How entries are kept fresh

Sage uses the server's tracking so that when a key you have cached changes, the server pushes an invalidation and the entry is dropped. The TTL you pass is a second bound: an entry is evicted once it expires even if no invalidation arrives. Between those two, a cached read returns the local value without a round-trip.

What can be cached

A read is cacheable when its result depends only on the current value of its keys. The server can then invalidate the result whenever one of those keys changes. Reads that vary with time (TTL, OBJECT IDLETIME) or are non-deterministic (SRANDMEMBER) are read-only but not cacheable because a key change cannot reliably invalidate them.

WARNING

cached rejects writes and keyless reads with NotCacheable. The server cannot invalidate a keyless read when data changes, which could leave a stale value in the cache.

Tune cache sizing and behavior through clientCache on SageConfig.

Topology

Caching works on every topology, with no change to your code. A cached read always runs against a master, never a replica whatever the read policy, since that is where the tracking-backed cache lives. In a cluster each slot-owning master keeps its own cache, and the read is routed to the master owning the key's slot.

Limitations

  • The cache budget is per master, not global: clientCache.maxBytes sizes each master's cache, so a cluster's effective ceiling is maxBytes times the number of masters.
  • A cache goes cold whenever its connection is replaced or its master stops owning the slot: a reconnect, a cluster failover, or a resharding that moves the slot off its master all start it fresh.
  • During a live slot migration a cached read of a migrating key runs uncached (following the ASK redirect once) until the migration completes.
  • A server that speaks RESP3 but rejects CLIENT TRACKING (an ACL or proxy restriction) still connects; cached reads there run uncached, exactly as when caching is disabled.