Skip to main content
SEMastery
ASP.NETintermediate

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.

11 min readUpdated November 3, 2025

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.

Figure 1: The cache-aside pattern. Check the cache first; only touch the database on a miss, then save the result for next time.

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

In-Memory
Distributed (Redis)
HybridCache
Output Cache

Steps

1

In-Memory

Stores data in one server's RAM — fast, not shared

2

Distributed

Stores data in Redis — shared across all servers

3

HybridCache

L1 memory + L2 Redis, with stampede protection

4

Output Cache

Caches the whole HTTP response, not just data

In-memory is fastest but per-server. Distributed is shared. HybridCache combines both. Output cache stores whole responses.

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.

Figure 2: In-memory cache is per-server and not shared. A Redis distributed cache is shared by all servers behind the load balancer.

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?

CacheStoresShared across servers?Best for
In-memoryData objectsNoSingle server, simple apps
Distributed (Redis)Data objectsYesMany servers behind a load balancer
HybridCacheData objects (L1 + L2)Yes (with L2)New apps wanting speed + sharing
Output cacheWhole responsesOptional (Redis)Public, rarely-changing responses

A simple decision guide:

Your situationUse…
One server, data fits in RAMIn-memory
Several servers, need a shared cacheRedis (distributed)
New .NET app, want both speed and sharingHybridCache
Same response served to everyoneOutput 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").

Figure 3: A cache stampede. When a hot key expires, many requests miss together and flood the database. Stampede protection lets only one through.

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

Expiration
Evict on change
Tag-based
Stay fresh

Steps

1

Expiration

Items expire after a set time — simple safety net

2

Evict on change

Remove the key when the real data is updated

3

Tag-based

Tag related items and clear them together (HybridCache)

4

Fresh

Combine all three for fast and correct data

Combine time-based expiration with eviction on change for safe, fresh caching. Use tags to clear groups of related items.

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:42 is 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

Related Posts