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.
A notebook on your desk
Imagine you work at a help desk, and people keep asking you the same question: "What are today's office timings?" The answer is written in a thick rulebook on a far shelf. If you walk to the shelf and look it up every single time, you waste minutes on each person and the queue grows long.
A smarter clerk writes the answer on a sticky note and keeps it on the desk. Now, when someone asks, you read the note instantly — no walk to the shelf. You only go back to the rulebook when the note is missing or the timings change.
That sticky note is a cache. In a web app, the "rulebook on the far shelf" is your database or a slow service, and the "sticky note on the desk" is fast memory. Caching saves the result of expensive work so you can return it instantly next time, instead of doing the slow work again and again.
ASP.NET Core gives you several kinds of caches. Let us understand each one with simple pictures, and learn which to use when.
How caching works: the cache-aside pattern
The most common caching style is called cache-aside. The logic is simple: before doing slow work, check the cache. If the answer is there (a "cache hit"), return it. If not (a "cache miss"), do the work, save it in the cache, and return it.
The first request is slow (it misses and hits the database). Every request after that is fast (it hits the cache) until the cached copy expires. For data that is read far more often than it changes — product details, settings, lookup lists — this is a huge speed-up. The bigger the gap between how often data is read and how often it changes, the more caching helps: a settings value read a million times an hour but changed once a week is the perfect thing to cache.
The four caching options in ASP.NET Core
ASP.NET Core offers four main tools. They differ in where the data is stored and what they cache.
Caching Options in ASP.NET Core
Steps
In-Memory
Stores data in one server's RAM — fast, not shared
Distributed
Stores data in Redis — shared across all servers
HybridCache
L1 memory + L2 Redis, with stampede protection
Output Cache
Caches the whole HTTP response, not just data
1. In-memory cache
The simplest cache. IMemoryCache stores objects in the RAM of the current server. It is extremely fast because there is no network involved.
public class ProductService(IMemoryCache cache, AppDbContext db)
{
public async Task<Product?> GetProduct(int id)
{
return await cache.GetOrCreateAsync($"product:{id}", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
return await db.Products.FindAsync(id); // runs only on a miss
});
}
}The catch: the data lives in one server's memory. If you run several servers behind a load balancer, each has its own separate cache — they do not share. That is fine for a single server, but not for a scaled-out app.
2. Distributed cache with Redis
When you run many servers, you want them all to share one cache. A distributed cache stores data in a separate shared store, most commonly Redis. Every server reads and writes the same cache.
// In Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});Redis lives outside your app, so the cache survives app restarts and is shared by all instances. The small cost is a network hop to Redis — still far faster than a database query, but not as instant as local RAM.
3. HybridCache: the best of both
Choosing between fast-but-private memory and shared-but-slower Redis is annoying — so HybridCache gives you both. It keeps a fast L1 copy in memory and an optional L2 copy in Redis. It checks memory first, then Redis, then your database.
public class ProductService(HybridCache cache, AppDbContext db)
{
public async Task<Product?> GetProduct(int id)
{
return await cache.GetOrCreateAsync(
$"product:{id}",
async token => await db.Products.FindAsync([id], token),
new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(10) });
}
}HybridCache also adds two things you would otherwise build by hand:
- Stampede protection. If 100 requests miss the same key at once, only one runs the database query while the other 99 wait for that single result. Without it, all 100 would hammer the database together.
- Tag-based invalidation. You can tag cached items and clear a whole group at once — handy when related data changes.
For new .NET projects, reach for HybridCache first. It gives you in-memory speed, optional Redis sharing, and stampede protection with very little code — instead of wiring IMemoryCache and IDistributedCache together yourself.
4. Output cache
The three caches above store data. Output caching stores the whole HTTP response — so a repeated request can skip your controller entirely and return the saved response.
// In Program.cs
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseOutputCache();
app.MapGet("/products", GetAllProducts)
.CacheOutput(p => p.Expire(TimeSpan.FromMinutes(5)));This is perfect for public pages or API responses that are the same for everyone and change rarely. For Redis-backed output caching across many servers, use AddStackExchangeRedisOutputCache.
For pages that differ per user, be careful with output cache. Use SetVaryByValue with the user id, or you might accidentally show one user's data to another. When in doubt, do not output-cache authenticated, user-specific responses.
Which cache should you use?
| Cache | Stores | Shared across servers? | Best for |
|---|---|---|---|
| In-memory | Data objects | No | Single server, simple apps |
| Distributed (Redis) | Data objects | Yes | Many servers behind a load balancer |
| HybridCache | Data objects (L1 + L2) | Yes (with L2) | New apps wanting speed + sharing |
| Output cache | Whole responses | Optional (Redis) | Public, rarely-changing responses |
A simple decision guide:
| Your situation | Use… |
|---|---|
| One server, data fits in RAM | In-memory |
| Several servers, need a shared cache | Redis (distributed) |
| New .NET app, want both speed and sharing | HybridCache |
| Same response served to everyone | Output cache |
The cache stampede problem
There is one nasty trap worth understanding. Imagine a very popular item — say the homepage product list — is cached and getting thousands of hits a second, all served instantly from cache. Then the cache entry expires. Suddenly, the next thousand requests all miss the cache at the same moment, and they all rush to the database together. The database, used to zero traffic for this query, gets a thousand identical queries in one instant and may fall over. This is called a cache stampede (or "thundering herd").
This is exactly why HybridCache is so valuable: it has built-in stampede protection. When many requests miss the same key together, only one runs the database query, and the rest simply wait for that single result. If you use plain IMemoryCache or IDistributedCache for very hot keys, you have to build this protection yourself (often with a lock), or risk the stampede.
Invalidation strategies at a glance
Three Ways to Keep a Cache Fresh
Steps
Expiration
Items expire after a set time — simple safety net
Evict on change
Remove the key when the real data is updated
Tag-based
Tag related items and clear them together (HybridCache)
Fresh
Combine all three for fast and correct data
The hard part: cache invalidation
Caching has a famous saying: "There are only two hard things in computer science: cache invalidation and naming things." The tricky question is — when the real data changes, how do you make sure the cache does not keep serving the old copy?
Three common answers:
- Expiration (time-based). Let cached items expire after a set time, like 10 minutes. Simple, and good enough for data that can be slightly stale.
- Eviction on change. When you update the data, remove (or update) the matching cache key so the next read fetches fresh data.
- Tag-based invalidation. Tag related items and clear them as a group — HybridCache supports this directly.
Always set a sensible expiration, even if you also evict on change. It is a safety net: if you ever forget to clear a key, expiration guarantees the stale data cannot live forever.
Best practices
- Cache read-heavy, rarely-changing data. The bigger the read-to-write ratio, the more caching helps. Do not cache data that changes every second.
- Always set an expiration. It bounds how stale data can get and protects you from forgotten keys.
- Use clear, consistent keys. Something like
product:42is easy to read, debug, and invalidate. - Do not cache huge objects. Caching very large items can use more memory than it saves. Cache the small, hot things.
- Watch your hit rate. A high cache-hit ratio means the cache is doing its job. A low one means you are caching the wrong things or expiring too soon.
Quick recap
- Caching saves expensive results so you return them instantly next time — like a sticky note instead of walking to the rulebook.
- In-memory cache is fastest but lives on one server; Redis distributed cache is shared across servers; HybridCache combines both with stampede protection; output cache stores whole responses.
- The cache-aside pattern checks the cache first and only touches the database on a miss.
- For new apps, start with HybridCache; use plain in-memory for single servers and Redis when scaling out.
- The hardest part is invalidation — always set an expiration, evict on change, and use tags when you can.
Keep the answers your app needs most on a sticky note close at hand, refresh them when they change, and your application will feel fast even under heavy load. Done well, caching is one of the cheapest and biggest performance wins you can add: a few lines of code can turn a slow, database-heavy endpoint into one that answers in a millisecond. Start with HybridCache for new work, cache the read-heavy data your users hit most, always set an expiration, and watch your hit rate climb — your database and your users will both thank you.
References and further reading
- Overview of caching in ASP.NET Core — Microsoft Learn — the official starting point.
- Distributed caching in ASP.NET Core — Microsoft Learn — how to set up Redis and
IDistributedCache. - HybridCache in ASP.NET Core — Milan Jovanović — a clear guide to the newer HybridCache library.
Related Posts
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.
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.
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.