HybridCache in ASP.NET Core: The New Caching Library Explained
Learn HybridCache in ASP.NET Core the simple way: two-level caching, stampede protection, tag invalidation, GetOrCreateAsync, and Redis setup with clear diagrams.
The shared fridge in a hostel kitchen
Imagine a hostel where many students share one kitchen. Each student has a small lunchbox at their own desk. That lunchbox is super fast to reach, but it is yours alone — nobody else can see what is inside.
The kitchen also has one big shared fridge. It is a little slower to walk to, but everyone in the hostel can use it. If one student stocks it with milk, every other student finds that milk waiting there.
Now picture the smartest student. When she wants milk, she first peeks in her own lunchbox. If it is there, great — instant. If not, she walks to the shared fridge. If the fridge has it, she takes some and also keeps a bit in her lunchbox for next time. Only if even the fridge is empty does she walk all the way to the shop, buy milk, and then refill both the fridge and her lunchbox.
That is exactly what HybridCache does. Your lunchbox is the fast in-memory cache (L1). The shared fridge is the distributed cache (L2), like Redis. The shop is your database. HybridCache checks the fast layer, then the shared layer, then the source — and refills as it goes. One simple helper does all of this for you.
What problem is HybridCache solving?
Before .NET 9, ASP.NET Core gave us two separate tools, and each had a weak side.
IMemoryCacheis very fast because it stores objects right inside your app's memory. But it only lives on one server. If you run two or three copies of your app behind a load balancer, each copy has its own private cache. They do not share.IDistributedCachefixes sharing. It stores bytes in something like Redis that all servers can read. But it is slower (a network hop), and you have to write the boring plumbing yourself: check the cache, serialize, deserialize, handle misses, and store again.
People kept writing the same glue code over and over. And almost nobody handled the cache stampede problem correctly. HybridCache, introduced in .NET 9 and now generally available, wraps both layers behind one clean API and solves the stampede problem for you.
How the two layers work together
When you ask HybridCache for a value by its key, it follows a clear order. It is the same order our smart student used in the hostel kitchen.
HybridCache lookup order
Steps
L1 memory
Check fast in-process cache first
L2 Redis
If missed, check shared distributed cache
Factory
If still missed, run your data-fetch method once
Store back
Save the result into L2 and L1
The big win is that you do not write this flow by hand. You give HybridCache a key and a factory (a small method that knows how to fetch the data). HybridCache decides whether it even needs to call your factory.
Installing and registering HybridCache
First, add the NuGet package. As of now it is shipped under the Microsoft.Extensions.Caching.Hybrid package.
dotnet add package Microsoft.Extensions.Caching.HybridThen register it in your Program.cs. The simplest setup needs just one line.
var builder = WebApplication.CreateBuilder(args);
// Register HybridCache (uses in-memory L1 by default)
builder.Services.AddHybridCache();
var app = builder.Build();
app.Run();That is enough to start. With only this, HybridCache uses its in-memory layer. If you never add a distributed cache, it simply runs as a smart, single-server cache with stampede protection — still a clear upgrade over raw IMemoryCache.
To add the shared layer, register a distributed cache too. HybridCache will pick it up automatically as L2.
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
builder.Services.AddHybridCache();You also need the Redis package for this:
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedisUsing GetOrCreateAsync — the one method you need
The heart of HybridCache is GetOrCreateAsync. You pass a key and a factory. HybridCache returns the cached value if it has one, or runs your factory once and caches the result.
public class ProductService(HybridCache cache, AppDbContext db)
{
public async Task<Product?> GetProductAsync(int id, CancellationToken ct = default)
{
return await cache.GetOrCreateAsync(
$"product:{id}", // the cache key
async token => // the factory (only runs on a miss)
{
return await db.Products
.FirstOrDefaultAsync(p => p.Id == id, token);
},
cancellationToken: ct);
}
}Read that factory carefully. It only runs when the value is not already cached. On a hit, your database is never touched. Notice the token passed into the factory — HybridCache gives you a combined cancellation token, which matters for stampede protection (more on that next).
In an endpoint, you just call the service like normal:
app.MapGet("/products/{id}", async (int id, ProductService service, CancellationToken ct) =>
{
var product = await service.GetProductAsync(id, ct);
return product is null ? Results.NotFound() : Results.Ok(product);
});For a route like GET /products/{id}, the first call fills the cache and every call after that is served from memory or Redis until the entry expires.
Stampede protection: the quiet hero
Here is a problem that hits real systems hard. Suppose a popular product page is not cached yet — maybe it just expired. Now 500 users request it in the same second. With plain IMemoryCache, all 500 requests see a miss and all 500 hit the database at the same time. Your database can fall over. This is a cache stampede (also called the "thundering herd").
HybridCache handles this for you. For a given key, only one caller runs the factory. The other 499 callers wait for that single result and then share it. One database call instead of 500.
Stampede protection in action
Steps
500 requests
All ask for the same missing key
1 runs factory
HybridCache lets only one fetch data
499 wait
Others pause for that single result
All get result
One DB hit, shared by everyone
This is the single biggest reason to prefer HybridCache. You get this safety with zero extra code.
Controlling expiration with HybridCacheEntryOptions
You usually do not want data to live forever. HybridCache lets you set two timings per entry.
- Expiration is the total lifetime across both layers (including L2/Redis).
- LocalCacheExpiration is how long the value may stay in the fast L1 memory layer.
var options = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10), // overall lifetime
LocalCacheExpiration = TimeSpan.FromMinutes(2) // L1 memory lifetime
};
var product = await cache.GetOrCreateAsync(
$"product:{id}",
async token => await db.Products.FindAsync([id], token),
options,
cancellationToken: ct);A short L1 time with a longer overall time is a common pattern. The memory copy stays fresh-ish, while Redis keeps a copy a bit longer so other servers benefit too.
Tags: invalidate many entries at once
Sometimes one change should clear many cached items. For example, when a product category changes, every product in it may need refreshing. Deleting keys one by one is painful. HybridCache lets you attach tags to entries and then wipe a whole tag in a single call.
var product = await cache.GetOrCreateAsync(
$"product:{id}",
async token => await db.Products.FindAsync([id], token),
tags: ["products", $"category:{categoryId}"],
cancellationToken: ct);Later, when something in that category changes:
// Remove every entry tagged "category:42" in one shot
await cache.RemoveByTagAsync($"category:{categoryId}", ct);You can also remove a single key directly:
await cache.RemoveAsync($"product:{id}", ct);One honest caveat to remember: when you invalidate by key or by tag, the change applies to the current server and to the shared L2 store. The in-memory (L1) copies sitting on other servers are not instantly cleared — they simply expire on their own schedule. So keep your LocalCacheExpiration short if you need other servers to notice changes quickly.
Comparing the caching options
This table shows where HybridCache fits next to the older tools.
| Feature | IMemoryCache | IDistributedCache | HybridCache |
|---|---|---|---|
| Speed | Fastest (in-process) | Slower (network) | Fast L1 + shared L2 |
| Shared across servers | No | Yes | Yes (via L2) |
| Stampede protection | No | No | Yes (built in) |
| Tag invalidation | No | No | Yes |
| Serialization handled | N/A | Manual | Automatic |
| One simple API | Partly | No | Yes |
And here is a quick guide on which method to reach for.
| You want to... | Use |
|---|---|
| Read with cache, fill on miss | GetOrCreateAsync |
| Store a value you already have | SetAsync |
| Remove one entry by key | RemoveAsync |
| Remove many entries by tag | RemoveByTagAsync |
Serialization: how objects become cache bytes
The L1 memory layer can keep your object as-is. But the L2 layer (Redis) only stores bytes. So HybridCache must turn your object into bytes and back. By default it uses JSON for your own types and a simple serializer for strings and byte arrays.
If you need something faster or smaller, you can plug in your own serializer, such as Protobuf. You register it when calling AddHybridCache.
builder.Services.AddHybridCache()
.AddSerializer<MyDto, MyProtobufSerializer>();For most apps the default JSON is perfectly fine. Reach for a custom serializer only when measurements show it matters.
A few good habits
- Pick clear, unique keys. Use a prefix and an id, like
product:42. This avoids accidental collisions. - Cache the right things. Data that is read a lot and changes rarely is the best fit — reference lists, product details, settings.
- Do not cache user secrets carelessly. A shared cache like Redis is visible to all servers; be thoughtful about what you put there.
- Keep L1 short on multi-server setups so changes spread quickly enough for your needs.
- Always pass the cancellation token through to your factory, so cancelled requests do not waste database work.
When HybridCache might be overkill
HybridCache is great, but you do not always need it. If your app runs on a single server and your data is tiny, plain IMemoryCache may be simpler. If you are caching whole HTTP responses rather than individual objects, output caching is often a better tool. HybridCache shines when you cache objects that are expensive to build and you run on more than one server — or when you simply want stampede protection without writing it yourself.
Note that some popular libraries in the .NET space have changed their licensing recently — for example, MediatR and MassTransit moved to commercial licenses. HybridCache is not one of those; it is part of the official Microsoft.Extensions packages and is free to use.
Quick recap
- HybridCache (new in .NET 9) joins a fast in-memory layer (L1) and an optional shared distributed layer (L2, like Redis) behind one API.
- The core method is
GetOrCreateAsync: it returns cached data or runs your factory once on a miss, then stores the result in both layers. - Stampede protection is built in — for one key, only one caller runs the factory while the rest wait and share the result.
- Tags let you invalidate many entries at once with
RemoveByTagAsync; useRemoveAsyncfor a single key. - Set lifetimes with
HybridCacheEntryOptions(Expirationfor the whole entry,LocalCacheExpirationfor the L1 copy). - It works without Redis; adding a distributed cache simply enables the shared L2 layer.
- Remember the caveat: invalidation does not instantly clear L1 copies on other servers, so keep local expiration short when needed.
References and further reading
- HybridCache library in ASP.NET Core — Microsoft Learn
- Hello HybridCache! Streamlining Cache Management — .NET Blog
- Caching in .NET — Microsoft Learn
- HybridCache in ASP.NET Core, New Caching Library — Milan Jovanovic
- Hybrid Caching in ASP.NET Core — Code Maze
Related Posts
Caching in ASP.NET Core: Make Your App Fast (The Easy Way)
Learn caching in ASP.NET Core with simple examples. Understand in-memory cache, distributed Redis cache, HybridCache, and output cache, with diagrams, code, and clear advice on which to use and when.
ASP.NET Core Output Cache: Speed Up Your API with In-Memory and Redis
Learn ASP.NET Core output caching the easy way: cache whole API responses in memory or in Redis, set policies, vary by query, and clear with tags — with diagrams and code.
How to Implement Caching Strategies in .NET: A Beginner-Friendly Guide
Learn caching strategies in .NET with simple examples: in-memory cache, distributed Redis cache, and HybridCache, plus eviction, stampede protection, and tags.
How to Increase the Performance of Web APIs in .NET
A friendly, step-by-step guide to making your ASP.NET Core Web APIs fast: async, caching, query tuning, compression, and pooling in .NET 10.
Make Your ASP.NET Core Web API 18x Faster with HybridCache
Learn how HybridCache in .NET 9 speeds up your ASP.NET Core Web API up to 18x, with L1/L2 caching, stampede protection, and tags, explained simply.
Solving Distributed Cache Invalidation with Redis and HybridCache
Learn how Redis and HybridCache solve distributed cache invalidation in ASP.NET Core with tags, backplanes, and a simple kitchen-counter analogy.