The gap
Redis 6 shipped CLIENT TRACKING in 2020: opt a connection in, and the server pushes an invalidation the moment a key you've read changes anywhere. It's the building block for a correct client-side
cache — no TTL guessing, no serving stale data because nobody told you it changed.
StackExchange.Redis never implemented it. The issue has been open since 2020([#1461(https://github.com/StackExchange/StackExchange.Redis/issues/1461)), and the 3.x rewrite still ships without it.
Why not just fork it
Forking means tracking every upstream fix and release forever. Instead, RedisNearCache is a package that sits next to StackExchange.Redis: your existing multiplexer keeps doing exactly what it does today, and a second connection — owned and managed by RedisNearCache — handles the tracking and invalidation side-channel.
services.AddRedisNearCache("localhost:6379");
var cache = provider.GetRequiredService<IRedisNearCache>();
await cache.Ready;
var user = await cache.GetAsync<User>("user:42"); // miss: GET + tracked + stored in L1
var again = await cache.GetAsync<User>("user:42"); // hit: served from memory, no round trip
A write to user:42 from any client — not just this process — evicts the local copy within milliseconds:
await cache.SetAsync("user:42", user with { Name = "Ada" });
Two invalidation paths, one API
Redis reached directly (self-hosted, ElastiCache node-based, Azure Cache for Redis) supports REDIRECT-based tracking, which RedisNearCache uses by default. Redis Enterprise-based services
(Azure Managed Redis, Redis Cloud, Redis Software) sit behind a proxy that needs Broadcast mode instead — a separate RESP3 connection per master, subscribed to invalidations for a key prefix you register:
services.AddRedisNearCache("my-cache.region.redis.azure.net:10000,ssl=true,password=<key>", o =>
{
o.TrackingMode = TrackingMode.Broadcast;
o.KeyPrefixes.Add("user:");
});
Everything after registration — GetAsync, SetAsync, statistics — is identical in both modes. It also drops in behind HybridCache and IDistributedCache if that's what your code already targets.
Does it actually help?
Benchmarked against a plain IMemoryCache with a TTL, HybridCache with a Redis L2, and FusionCache with a backplane — 20 app instances, 160 readers, 2,000 writes/s arriving from a separate service:
| Reads/s | Reads served stale | Stalest read | |
|---|---|---|---|
| RedisNearCache | 2.20M | 0.33% | 105ms |
| IMemoryCache + 10s TTL | 14.51M | 83.1% | 10.0s |
| HybridCache + Redis L2 | 25.99M | 84.8% | 10.0s |
| FusionCache + backplane | 6.42M | 85.0% | 10.0s |
The TTL-based options read faster in raw throughput, but they're serving stale data over 80% of the time in this workload — some reads up to the full TTL window old. RedisNearCache trades some of that
throughput for actually knowing when a value changed. Full methodology, cluster/TLS/Valkey/chaos runs, and per-call cost breakdowns are in the benchmark README.
Try it
dotnet add package RedisNearCache
- GitHub: https://github.com/magna-nz/redis-near-cache
- Docs: https://magna-nz.github.io/redis-near-cache/
- NuGet: https://www.nuget.org/packages/RedisNearCache
Issues, especially from anyone running Azure Managed Redis, Redis Cloud, or Redis Software who can put Broadcast mode through its paces, are very welcome.
Top comments (1)
The failure mode I’d want surfaced is invalidation-channel disconnect and reconnect. A near cache can be correct while connected and silently become TTL-like after missing pushes. I’d expose a cache-coherence state or metric and either bypass L1 or bound entries with a short TTL until tracking is re-established, then chaos-test writes during reconnection. Does RedisNearCache flush local entries when the tracking connection is lost, or otherwise detect gaps before resuming cached reads?