HybridCache: One API for In-Memory + Distributed Caching

Caching in ASP.NET Core has always meant choosing between IMemoryCache (fast, single-node) and IDistributedCache (shared, slower). In practice you want both: a fast L1 in-memory layer backed by shared L2 storage like Redis. But wiring them together manually means duplicate code, stampede bugs, and serialization headaches.

HybridCache in .NET 9 wraps both layers behind a single call.

Setup

Terminal window
dotnet add package Microsoft.Extensions.Caching.Hybrid
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10), // L2 (distributed)
LocalCacheExpiration = TimeSpan.FromMinutes(2) // L1 (memory)
};
});
// Optional: add a distributed backend
builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");

Without an IDistributedCache registration, HybridCache still works as an in-memory cache with stampede protection.

Basic Usage

app.MapGet("/products/{id:int}", async (int id, HybridCache cache) =>
{
var product = await cache.GetOrCreateAsync(
$"product:{id}",
async cancel => await LoadProductFromDb(id, cancel));
return product is not null ? Results.Ok(product) : Results.NotFound();
});

First call: factory runs, result is stored in L1 and L2. Subsequent calls within the local expiration window come straight from memory without touching Redis.

Stampede Protection

This is the headline feature. If 100 concurrent requests all miss the cache for the same key, only one runs the factory. The other 99 wait for that single result. No configuration needed. It’s the default behavior.

Tag-Based Invalidation

Group related entries with tags and invalidate them in one call:

// When caching, attach a tag
var products = await cache.GetOrCreateAsync(
$"products:category:{category}",
async cancel => await LoadByCategory(category, cancel),
tags: ["products"]);
// When data changes, evict everything tagged "products"
await cache.RemoveByTagAsync("products");

For single entries, RemoveAsync clears from both L1 and L2:

await cache.RemoveAsync($"product:{id}");

Key Takeaway

HybridCache gives you L1 + L2 caching, stampede protection, and tag-based invalidation behind a single GetOrCreateAsync call. Stop hand-rolling two-layer cache logic.