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.
Imagine a busy tea stall outside a railway station. Every morning the same customers ask for the same masala chai. If the chaiwala boiled a fresh pot from scratch for every single cup, the line would never move. So he keeps a hot pot ready. When you ask, he pours from the pot. Fast. Only when the pot runs out does he boil a new one.
A cache is exactly that hot pot of tea. Your API keeps the answer ready, so it does not have to "boil" it (go to the database) every time someone asks.
In .NET 9, Microsoft gave us a brand new tool for this called HybridCache. It is smart, it is simple, and in many real APIs it makes responses many times faster. In this guide we will learn what it is, why it is special, and how to use it. We will keep the words easy and the steps small.
Why caching makes APIs faster
When your API gets a request, it often does slow work. It talks to a database. It calls another service. It does heavy math. All of that takes time, maybe 100 milliseconds or more.
But here is the secret: a lot of requests ask for the same thing. The same product. The same user profile. The same list of cities. If the answer does not change every second, why do the slow work again and again?
Caching means: do the slow work once, keep the answer in fast memory, and hand it out instantly next time. Reading from memory takes microseconds, not milliseconds. That is the gap that gives you an 18x, or even bigger, speed-up.
The two kinds of cache, and why "hybrid"
Before HybridCache, .NET gave us two separate caches, each with a weakness.
- In-memory cache (
IMemoryCache) keeps data inside the app's own memory. It is blazing fast. But it is private to one server. If you run five copies of your API, each has its own little cache, and they do not share. - Distributed cache (
IDistributedCache), usually Redis, keeps data in a shared outside store. All servers can see it. But reaching it goes over the network, so it is slower than local memory, and you must write code to serialize objects yourself.
HybridCache says: why pick one? Use both. The fast local memory is called L1. The shared distributed store is called L2.
HybridCache checks L1 first. If it is there, done, instantly. If not, it checks L2. If L2 has it, it copies it back into L1 and returns it. Only if both miss does it do the real slow work, and then it fills both layers for next time. You get the speed of memory and the sharing of Redis, with one simple call.
The best part: if you never configure Redis, HybridCache happily runs with just L1. You can add L2 later without changing your code.
Getting started: install the package
HybridCache lives in a NuGet package. Add it to your project:
dotnet add package Microsoft.Extensions.Caching.HybridThen register it in Program.cs. This one line wires everything up:
var builder = WebApplication.CreateBuilder(args);
// Register HybridCache with default settings.
builder.Services.AddHybridCache();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();That is it. With no Redis configured, HybridCache uses the in-memory MemoryCache for storage. Your app is now ready to cache.
The one method you need: GetOrCreateAsync
The heart of HybridCache is a method called GetOrCreateAsync. Its name tells the whole story: get the value if it is cached, or create it (and cache it) if it is not.
You give it two things: a key (a unique name for the data) and a factory (the slow work to run on a miss). HybridCache handles the rest.
Here is a small product API. Pretend GetProductFromDatabaseAsync is slow.
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly HybridCache _cache;
private readonly IProductRepository _repository;
public ProductsController(HybridCache cache, IProductRepository repository)
{
_cache = cache;
_repository = repository;
}
[HttpGet("{id}")]
public async Task<Product> GetProduct(int id, CancellationToken ct)
{
return await _cache.GetOrCreateAsync(
$"product-{id}", // the cache key
async token => // the slow factory
{
return await _repository.GetProductFromDatabaseAsync(id, token);
},
cancellationToken: ct);
}
}The first time someone asks for GET /api/products/5, the cache is empty, so the factory runs and hits the database. Every request after that, until the entry expires, gets the answer straight from memory. No database. No serialization code. No fuss.
Notice how little we wrote. The old cache-aside pattern, check the cache, deserialize, on miss query the database, serialize, store, was a dozen lines and easy to get wrong. HybridCache shrinks it to one call.
How GetOrCreateAsync works
Steps
Check L1
Look in fast local memory first
Check L2
On miss, look in shared Redis
Run factory
On full miss, do the slow work once
Fill caches
Store result in both L1 and L2
Return
Hand the value back to the caller
Where the 18x speed-up comes from
Let us make the numbers real. Say one product lookup takes about 90 milliseconds from the database, but only about 5 milliseconds from the in-memory cache. That alone is roughly 18 times faster for a cache hit.
| Step | Without cache | With HybridCache (hit) |
|---|---|---|
| Find data | Query database (~85 ms) | Read from memory (~4 ms) |
| Convert to object | Map rows, deserialize | Reuse cached object |
| Total time | ~90 ms | ~5 ms |
| Database load | Every request | Almost zero |
The speed-up is not magic, it is simply avoiding slow work. And the busier your API gets, the bigger the win, because more requests can reuse the same hot answer. Your database also breathes easier, which keeps the whole system stable under load.
Stampede protection: the hidden superpower
Here is a problem that bites real APIs hard. Imagine a popular product. Its cache entry expires. At that exact second, 100 requests arrive for it. With a plain IMemoryCache, all 100 see an empty cache, and all 100 fire the same database query at the same moment. The database gets hammered. This pile-up is called a cache stampede (or "thundering herd").
HybridCache solves this automatically. When many callers ask for the same missing key at once, it runs the factory exactly one time. The other callers simply wait and then receive the same result. One database query instead of one hundred.
This single feature can save a database from falling over during a traffic spike. You get it for free, just by using GetOrCreateAsync.
Controlling how long things stay cached
You do not always want data to live forever. A price might change. A profile gets edited. HybridCache lets you set expiry times with HybridCacheEntryOptions.
There are two timers worth knowing:
- Expiration is the total time the entry can live across L1 and L2 (the overall lifetime).
- LocalCacheExpiration is how long the entry stays in the fast L1 memory before it must be re-checked against L2.
var options = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10), // overall lifetime
LocalCacheExpiration = TimeSpan.FromMinutes(2) // L1 freshness
};
var product = await _cache.GetOrCreateAsync(
$"product-{id}",
async token => await _repository.GetProductFromDatabaseAsync(id, token),
options,
cancellationToken: ct);A good rule for beginners: keep the L1 time short and the overall time longer. That way each server refreshes its local copy often, but you still avoid hitting the database for every request.
Tags: clearing many entries at once
Sometimes you need to throw away a whole group of cached entries together. Say a seller updates their entire catalogue. You do not want to delete each product key by hand. HybridCache supports tags for this.
When you cache an item, attach one or more tags. Later, invalidate every item with that tag in a single call.
// Cache with a tag.
var product = await _cache.GetOrCreateAsync(
$"product-{id}",
async token => await _repository.GetProductFromDatabaseAsync(id, token),
tags: new[] { "products", $"seller-{sellerId}" },
cancellationToken: ct);
// Later, when this seller changes their catalogue,
// clear everything tagged for that seller in one call.
await _cache.RemoveByTagAsync($"seller-{sellerId}", ct);To remove a single entry, use RemoveAsync with its key. Tags are for clearing groups; RemoveAsync is for clearing one.
When to clear the cache
Steps
Data changed
Something was edited or deleted
One item?
Use RemoveAsync with the key
Many items?
Use RemoveByTagAsync with a tag
Done
Next request rebuilds fresh data
Adding the L2 (Redis) layer for multiple servers
As long as you run a single server, L1 alone is great. But most real apps scale out to several servers behind a load balancer. Then each server's private L1 is not enough, you want them to share. That is where L2 comes in.
Add a distributed cache, and HybridCache automatically uses it as L2. No change to your GetOrCreateAsync calls.
// Add Redis as the shared L2 cache.
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
// HybridCache will now use Redis as L2 automatically.
builder.Services.AddHybridCache();Now if server A builds a value and stores it, server B can find it in Redis instead of querying the database again. The local L1 still serves repeat hits at full speed.
A simple before-and-after comparison
Let us compare the old hand-written way with the HybridCache way, side by side.
| Concern | Old manual caching | HybridCache |
|---|---|---|
| Lines of code | Many (check, serialize, store) | One call |
| Stampede protection | You build it yourself | Built in |
| L1 + L2 together | Wire up two caches by hand | Automatic |
| Tag invalidation | Track keys manually | RemoveByTagAsync |
| Serialization | You write it | Handled for you |
The HybridCache column wins on every row, and it is less code. That is why it has quickly become the default choice for new .NET 9 and .NET 10 APIs.
Tips to avoid common mistakes
A few friendly warnings so your first try goes smoothly:
- Keep keys unique and clear. Use a pattern like
product-{id}. If two different things share a key, you will get wrong data. (Note how we write the key in backticks here so the curly braces are safe in text.) - Do not cache things that must always be fresh. A bank balance or a live stock price should not be cached for long, if at all.
- Pick sensible expiry times. Too long and users see stale data. Too short and you lose the benefit. Start at a few minutes and adjust.
- Cache the smallest useful object. Caching huge objects fills memory fast.
- Remember to invalidate on change. When you update or delete data, clear its cache entry so readers get the new value.
Quick recap
- A cache keeps a ready answer so your API skips slow work, like a chaiwala keeping a hot pot of tea.
- HybridCache in .NET 9 combines fast local memory (L1) with shared distributed cache (L2) in one simple tool.
- The main method is
GetOrCreateAsync: it returns the cached value or runs your factory once and caches the result. - Cache hits can be many times faster than database calls, giving real APIs an 18x or larger speed-up.
- Stampede protection is built in: 100 requests for a missing key cause only one database call.
- Set lifetimes with
HybridCacheEntryOptions, and clear groups of entries using tags andRemoveByTagAsync. - Add Redis and it becomes your L2 automatically, no change to your caching code, so your app scales across many servers.
References and further reading
- HybridCache library in ASP.NET Core (Microsoft Learn)
- Hello HybridCache! Streamlining Cache Management (.NET Blog)
- HybridCache in ASP.NET Core, New Caching Library (Milan Jovanovic)
- How To Improve Performance 18x Using HybridCache (Anton DevTips)
Related Posts
Adding Real-Time Functionality to .NET Apps with SignalR
Learn ASP.NET Core SignalR step by step: hubs, clients, groups, and scaling with Redis or Azure, explained for absolute beginners.
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.
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.
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.
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.
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.