Skip to main content
SEMastery
ASP.NETbeginner

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.

11 min readUpdated November 14, 2025

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.

Without cache versus with cache

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 L1 and L2 layers

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.Hybrid

Then 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

Check L1
Check L2
Run factory
Fill caches
Return

Steps

1

Check L1

Look in fast local memory first

2

Check L2

On miss, look in shared Redis

3

Run factory

On full miss, do the slow work once

4

Fill caches

Store result in both L1 and L2

5

Return

Hand the value back to the caller

The path a request takes through HybridCache

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.

StepWithout cacheWith HybridCache (hit)
Find dataQuery database (~85 ms)Read from memory (~4 ms)
Convert to objectMap rows, deserializeReuse cached object
Total time~90 ms~5 ms
Database loadEvery requestAlmost 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.

Stampede protection in action

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

Data changed
One item?
Many items?
Done

Steps

1

Data changed

Something was edited or deleted

2

One item?

Use RemoveAsync with the key

3

Many items?

Use RemoveByTagAsync with a tag

4

Done

Next request rebuilds fresh data

Choosing the right invalidation tool

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.

Two servers sharing one Redis L2

A simple before-and-after comparison

Let us compare the old hand-written way with the HybridCache way, side by side.

ConcernOld manual cachingHybridCache
Lines of codeMany (check, serialize, store)One call
Stampede protectionYou build it yourselfBuilt in
L1 + L2 togetherWire up two caches by handAutomatic
Tag invalidationTrack keys manuallyRemoveByTagAsync
SerializationYou write itHandled 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 and RemoveByTagAsync.
  • 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

Related Posts