Skip to main content

Client-side caching

Respire can cache eligible Redis reads in-process while Redis invalidation pushes keep entries coherent across clients. Repeated reads skip command encoding, socket I/O, Redis execution, reply parsing, and network latency while the application keeps using the same typed API.

This is Redis's server-assisted cache—not a second application caching abstraction. Redis tracks the keys Respire reads, pushes only invalidations when those keys change, and Respire evicts them. The next caller refreshes lazily; Redis never pushes replacement values.

Enable it

Enable caching once on the client:

await using var redis = await RespireClient.ConnectAsync(new RespireOptions
{
Endpoints = { new RespireEndpoint("localhost") },
ClientSideCache = new(),
});

Existing typed APIs and catalog ExecuteAsync calls then use the cache transparently. This covers deterministic keyed reads across strings, keys, hashes, lists, sets, sorted sets, streams, bitmaps, geospatial indexes, Redis arrays, JSON, and vector sets. GET and MGET keep optimized per-key entries and partial-hit behavior; other replies use exact command-and-argument identities.

Missing keys are cached too. Replies are deep-owned internally and converted for each call, so enabling caching does not introduce shared mutable objects.

Why this is different

StackExchange.Redis 3.1.13 supports RESP3 and exposes keyspace notifications, but it does not provide an equivalent built-in server-assisted local response cache. Its keyspace-notification documentation presents notifications as a building block for an application-defined invalidation strategy. That approach requires Redis server configuration plus application-owned storage, subscription, node coverage, bounds, command eligibility, and invalidation-race handling.

Respire uses Redis CLIENT TRACKING directly. No notification channel or global notify-keyspace-events setting is required. Redis's own client-side caching support table does not currently list StackExchange.Redis and warns that exposing CLIENT TRACKING alone is not the same as implementing a client cache.

Measured impact

These results compare a local Respire cache hit with an ordinary StackExchange.Redis server read. They demonstrate the value of avoiding a network round trip—not a claim that Respire's uncached wire path is hundreds of times faster. Uncached Respire and StackExchange.Redis reads were statistically equivalent in the same net10 run.

net10 operationStackExchange.Redis server readRespire client-cache hitHit latency
GET, present186.5 μs151.5 ns0.081%
GET, missing185.9 μs129.5 ns0.070%
HGET186.8 μs466.5 ns0.250%
EXISTS185.6 μs387.3 ns0.209%

BenchmarkDotNet used Redis 8.10, two launches, three warmups, and three measured iterations on a GitHub-hosted Linux runner. See the official net8/net10 run and benchmark source.

Invalidation flow

read miss → Redis response → local entry
key changes → RESP3 invalidation push → local eviction
next read → Redis response → refreshed local entry

With OPTIN, Redis tracks only misses Respire deliberately sends with CLIENT CACHING YES. Local mutations also evict before and after execution. If tracking continuity is lost, Respire clears affected cache state instead of trusting entries whose invalidations may have been missed.

Bounds

Tune entry count, approximate owned bytes, and local TTL together:

ClientSideCache = new RespireClientSideCacheOptions
{
MaxEntries = 25_000,
MaxSizeBytes = 128L * 1024 * 1024,
TimeToLive = TimeSpan.FromMinutes(2),
},

An oversized response is returned without being cached. GetLeaseAsync participates without sharing lease ownership. GEOSEARCH with COUNT ... ANY is also excluded because Redis may return an arbitrary early subset. Only exact MEMORY USAGE ... SAMPLES 0 calls are cached; sampled size estimates bypass the cache. Nondeterministic, random, probabilistic, blocking, script/function, time-series, Search, and unkeyed commands bypass caching; so do batches and transactions. Unknown mutations conservatively flush local entries before dispatch and after awaited completion. Respire rejects raw commands that would change protocol, database, or tracking state while this feature is enabled.

ASP.NET Core registration

Respire.Extensions.DependencyInjection provides a helper on its mutable options builder:

builder.Services.AddRespire(options =>
{
options.Endpoints.Add(new RespireEndpoint("redis.internal"));
options.UseClientSideCaching();
});

Diagnostics

var cache = redis.ClientSideCache!;
var statistics = cache.GetStatistics();

Console.WriteLine($"{statistics.Hits} hits; {statistics.SizeBytes} bytes");
cache.Clear();

The Respire OpenTelemetry meter emits hit, miss, invalidation, eviction, and continuity-flush counters.

Consistency boundary

Respire rejects a stale read response when an invalidation races cache insertion. It also flushes after awaited local mutations and on detected connection loss, reconnect, redirect, and cluster topology retirement. ASK retries return their value without caching because Redis applies both ASKING and CLIENT CACHING YES to the next command. Like every server-assisted client cache, it cannot observe invalidations across an undetected network partition. Configure TCP keepalive or ConnectionIdleReadTimeout, and keep a finite local TTL, when bounded failure detection matters.