Hybrid cache = in-memory (L1) + Redis (L2). Reads hit L1 first (fast), then L2, then the data source. Redis keeps the cache consistent across multiple server instances.
How it works
client → L1 (memory) → L2 (redis) → database
↑ cache hit ↑ cache hit ↑ cache miss
- L1 (in-memory): fastest. local to each server process.
- L2 (redis): shared across all server instances. survives restarts.
- database: only called on a full cache miss.
Setup
Register hybrid cache with Redis as the distributed backend:
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(5)
};
});
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = "hybrid-cache-demo";
});
Cache entries expire from both L1 and L2 after 5 minutes.
Sample endpoint
app.MapGet("/v1/categories", async (ILookupService service) =>
{
var result = await service.GetCategoriesAsync();
return Results.Ok(result);
});
Load test results
Using bombardier — 10,000 requests, 125 concurrent connections:
Reqs/sec 6937 avg (peak: 82,646)
Latency p50: 2.62ms p95: 4.24ms p99: 1.36s
HTTP 2xx 10,000 / 10,000
Throughput 2.34 MB/s
The p99 spike (1.36s) is expected — those are cache misses hitting the data source cold.
Trade-offs
- ✅ Fast reads: L1 hit returns in microseconds
- ✅ Scalable: Redis shares cache across instances
- ✅ Simple API:
GetOrCreateAsynchandles all layers - ⚠️ Redis required: Adds infra dependency
- ⚠️ Brief inconsistency: Each server has its own L1; may serve stale data until L1 expires
- ⚠️ Cache invalidation: Not trivial — design expiry strategy upfront