DEV Community

Anton Martyniuk
Anton Martyniuk

Posted on Originally published at antondevtips.com

ASP.NET Core Output Cache: How to Speed Up Your API with In-Memory Cache and Redis

In ASP.NET Core, one of the most powerful and underused caching tools is the Output Cache middleware.

Though so many developers still don't know about it or how to use it effectively.

Output Cache is not the same as storing objects in IMemoryCache or IDistributedCache.
It operates at the HTTP response level, caching the full serialized response and serving it directly — without touching your handlers, your database, or your business logic.

The result is dramatically lower latency and reduced load on your infrastructure.

In this post, we will explore:

  • What Output Cache is and how it differs from IMemoryCache and IDistributedCache
  • How to set up Output Cache in ASP.NET Core
  • How to customize cache behavior with policies and options
  • How to evict cached responses using tags, keys, and full cache clearing
  • How to use Redis as the Output Cache store for distributed scenarios
  • How to handle caching safely in authenticated APIs to avoid leaking data between users

Let's dive in.


👉 Read original article on my newsletter: https://antondevtips.com/blog/aspnetcore-output-cache-how-to-speed-up-your-api-with-in-memory-cache-and-redis

What Is Output Cache and How Does It Differ from Other Caches

IMemoryCache is an in-process, key-value store that lives in the memory of your application.

You use it to cache any .NET object — a list, a domain model, a computed value.
You control what gets stored, how it is serialized, and when it expires.

It is fast because there is no network hop.
But it is local to a single instance of your app, so it does not work across multiple servers without extra coordination.

public class OrderService(IMemoryCache cache, OrdersDbContext db)
{
    public async Task<List<OrderSummary>> GetOrdersAsync(CancellationToken ct)
    {
        return await cache.GetOrCreateAsync("orders:all", async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);

            return await db.Orders
                .Select(o => new OrderSummary(o.Id, o.Status, o.TotalAmount))
                .ToListAsync(ct);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

IDistributedCache is an abstraction over an external cache store, usually Redis.
It stores byte arrays, so you serialize and deserialize your objects manually (or with a wrapper).

It works across multiple app instances because all instances share the same external store.
The downside is the added latency of a network call to the cache server.

Both IMemoryCache and IDistributedCache require you to write caching logic inside your service or handler.

You have to call the cache before your database query, check for a hit, store the result after a miss, and handle expiration yourself.
This adds boilerplate to every method you want to cache.

Output Cache works differently.

Instead of caching objects inside your application code, Output Cache intercepts the HTTP response at the middleware level.
It stores the full serialized response — the status code, headers, and body — and replays it on subsequent matching requests.

Your endpoint handler, database query, and business logic are never called when a cached response is available.
The middleware short-circuits the pipeline and writes the stored response directly.

This means you can add caching to existing endpoints with almost no changes to your application code.
You decorate an endpoint or controller with an attribute or a policy name, and the middleware handles the rest.

The built-in cache lock feature in Output Cache is particularly useful.
When multiple requests arrive for the same uncached resource at the same time, only one request is allowed through to execute the handler.

The others wait for the first response and then receive the cached copy.
This prevents the "thundering herd" problem, where a cache miss causes a spike of concurrent database queries.

Output Cache was introduced in .NET 7 and has been improved in further .NET versions.

Setting Up Output Cache in ASP.NET Core

To get started with Output Cache, install the following NuGet package:

dotnet add package Microsoft.AspNetCore.OutputCaching
Enter fullscreen mode Exit fullscreen mode

Register Output Cache services in Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Register OutputCache in DI
builder.Services.AddOutputCache();

var app = builder.Build();

// Add OutputCache Middleware
app.UseOutputCache();

app.MapControllers();

app.Run();
Enter fullscreen mode Exit fullscreen mode

The middleware must be placed after UseRouting (if you call it explicitly) and before MapControllers or any Minimal API endpoints like MapGet.

Now, let's define a simple Orders API example:

[ApiController]
[Route("api/orders")]
public class OrdersController(OrdersDbContext db) : ControllerBase
{
    [HttpGet]
    [OutputCache]
    public async Task<IActionResult> GetOrders(CancellationToken ct)
    {
        var orders = await db.Orders.ToListAsync(ct);
        return Ok(orders);
    }

    [HttpGet("{id:guid}")]
    public async Task<IActionResult> GetOrder(Guid id, CancellationToken ct)
    {
        var order = await db.Orders.FindAsync([id], ct);
        if (order is null) return NotFound();
        return Ok(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

To cache the GetOrders endpoint, we add the [OutputCache] attribute.

With this one attribute, the first request to GET /api/orders will execute the handler, call the database and store the response.
Every subsequent request within the default expiration window (60 seconds) will receive the cached response without hitting the database.

You can also cache endpoints in Minimal APIs by calling .CacheOutput() on the RouteHandlerBuilder:

app.MapGet("/api/orders", async (OrdersDbContext db, CancellationToken ct) =>
{
    var orders = await db.Orders.ToListAsync(ct);
    return Results.Ok(orders);
}).CacheOutput();
Enter fullscreen mode Exit fullscreen mode

👉 Read original article on my newsletter: https://antondevtips.com/blog/aspnetcore-output-cache-how-to-speed-up-your-api-with-in-memory-cache-and-redis

Top comments (0)