Skip to main content

ASP.NET Core Output Caching: Speed Up Your API

Learn ASP.NET Core output caching to dramatically speed up API responses. Complete tutorial with code examples, best practices, and pitfalls. Start now!

ASP.NET Core output caching is one of the fastest ways to make a slow API feel instant. If your endpoints repeatedly run the same database queries, serialize the same JSON, or recompute the same expensive results for every request, you are wasting CPU, memory, and money. In this tutorial, you will learn how output caching in ASP.NET Core works, how to configure it in .NET 8 and later, and the best practices and common pitfalls that separate a fast, reliable API from a buggy one that serves stale data.

Whether you are a beginner searching for "how to cache API responses in C#", an intermediate developer comparing response caching vs output caching, or a senior engineer tuning cache policies at scale, this guide has you covered with practical, runnable code.

What Is ASP.NET Core Output Caching?

Output caching stores the full HTTP response generated by your server and replays it for subsequent matching requests — without re-executing your controller action, minimal API handler, or middleware pipeline. Introduced as first-class middleware in .NET 7 and significantly improved in .NET 8, the output caching middleware lives entirely on the server, which gives you full control over when entries are stored and evicted.

This is a critical distinction. With output caching, you decide what gets cached and you can invalidate it on demand. Compare that to the older approach below.

Response Caching vs Output Caching: What's the Difference?

"Response caching vs output caching" is one of the most common questions developers ask, so let's clear it up:

  • Response caching sets HTTP headers (like Cache-Control) and relies on the client or downstream proxies to honor them. The server does not store anything. If a browser or CDN ignores the headers, you get no benefit, and you cannot forcibly invalidate a cached entry.
  • Output caching stores the response on the server. It works regardless of client behavior, supports programmatic invalidation via tags, and can cache responses even for authenticated requests when you configure it to.

For most APIs, ASP.NET Core output caching is the better choice because it is predictable and controllable. Use response caching headers in addition when you also want browsers and CDNs to cache.

How to Set Up Output Caching in ASP.NET Core

Getting started takes two lines in your Program.cs. You register the services with AddOutputCache and add the middleware to the pipeline with UseOutputCache.

var builder = WebApplication.CreateBuilder(args);

// 1. Register output caching services
builder.Services.AddOutputCache();

var app = builder.Build();

// 2. Add the middleware to the pipeline
app.UseOutputCache();

app.MapGet("/products", async (ProductService service) =>
{
    var products = await service.GetAllAsync();
    return Results.Ok(products);
})
.CacheOutput(); // 3. Opt this endpoint into caching

app.Run();

That's it. The first request hits your handler and executes the query; every request after that — for the default duration — is served instantly from the cache. The handler does not run again until the entry expires.

Why does this matter? An endpoint that took 200 ms to query and serialize now responds in well under a millisecond. Under load, that frees database connections and CPU for the requests that genuinely need fresh data.

Important: Middleware Order

A common pitfall is placing UseOutputCache() in the wrong position. It must come after UseRouting() (when used) and before the endpoints it should cache. If you also use CORS, call UseCors() before UseOutputCache(), otherwise CORS headers may be cached or missed incorrectly.

Configuring Cache Policies and Expiration

The default policy caches for a short period, but real applications need control. Define named policies in AddOutputCache and apply them per endpoint.

builder.Services.AddOutputCache(options =>
{
    // A reusable named policy
    options.AddPolicy("Expire60", policy =>
        policy.Expire(TimeSpan.FromSeconds(60)));

    // Change the default for all cached endpoints
    options.AddBasePolicy(policy =>
        policy.Expire(TimeSpan.FromSeconds(30)));
});

Apply a named policy by passing its name to CacheOutput:

app.MapGet("/products", GetProducts)
   .CacheOutput("Expire60");

// Or configure inline without a named policy
app.MapGet("/categories", GetCategories)
   .CacheOutput(policy => policy.Expire(TimeSpan.FromMinutes(5)));

On MVC and API controllers, use the attribute form instead:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [OutputCache(PolicyName = "Expire60")]
    public async Task<IActionResult> Get()
    {
        var products = await _service.GetAllAsync();
        return Ok(products);
    }
}

Varying the Cache by Query String, Headers, or Route

By default, output caching keys entries on the full request path and all query string values. But you often want to vary deliberately — for example, cache /products?page=1 separately from /products?page=2, while ignoring an irrelevant tracking parameter.

options.AddPolicy("PagedProducts", policy =>
    policy.Expire(TimeSpan.FromMinutes(2))
          .SetVaryByQuery("page", "pageSize")   // vary only on these
          .SetVaryByHeader("Accept-Language"));  // vary on culture

Why this is essential: if you forget to vary by a meaningful parameter, different users will receive each other's cached data. If you vary by too much (like a unique session token), your hit rate collapses to zero and caching does nothing. Tune VaryBy carefully — it is the single most important correctness decision in caching.

Cache Invalidation with Tags

The hardest problem in caching is knowing when to throw data away. Output caching solves this elegantly with tags. Assign one or more tags to cached responses, then evict everything with a given tag in a single call when the underlying data changes.

app.MapGet("/products", GetProducts)
   .CacheOutput(policy => policy
       .Expire(TimeSpan.FromMinutes(10))
       .Tag("products")); // tag this entry

// When a product is created or updated, evict the tag
app.MapPost("/products", async (
    Product product,
    ProductService service,
    IOutputCacheStore cache,
    CancellationToken ct) =>
{
    await service.AddAsync(product);

    // Invalidate every cached response tagged "products"
    await cache.EvictByTagAsync("products", ct);

    return Results.Created($"/products/{product.Id}", product);
});

This pattern — cache aggressively, then invalidate precisely on write — gives you the best of both worlds: blazing-fast reads and guaranteed-fresh data after every mutation. It is the cornerstone of high-performance ASP.NET Core APIs.

Scaling Output Caching Across Multiple Servers

By default, the output caching middleware stores entries in local server memory. That works perfectly for a single instance, but in a load-balanced or containerized environment each server keeps its own cache. Cache hit rates drop and an EvictByTagAsync call on one node does not clear the others.

For distributed scenarios, implement or plug in a shared IOutputCacheStore backed by Redis. A simplified Redis-backed registration looks like this:

builder.Services.AddStackExchangeRedisOutputCache(options =>
{
    options.Configuration =
        builder.Configuration.GetConnectionString("Redis");
});

builder.Services.AddOutputCache();

With a shared store, every instance reads and writes the same cache and tag-based invalidation propagates everywhere. Always plan for this early if you intend to scale horizontally — retrofitting distributed caching is far harder than designing for it up front.

Best Practices for ASP.NET Core Output Caching

  • Only cache idempotent GET requests. The middleware will not cache POST, PUT, or DELETE, and you should never try to force it.
  • Never cache personalized or authenticated responses globally. If a response depends on the logged-in user, either skip caching or vary by a user-specific key — otherwise you will leak one user's data to another. This is a serious security pitfall.
  • Set realistic expirations. Match the duration to how often the data actually changes. Reference data can cache for hours; a live inventory count for seconds.
  • Pair short expirations with tag invalidation so reads stay fast and writes stay correct.
  • Avoid caching huge or rarely repeated responses. Caching only pays off when the same response is served many times. A unique-per-request payload just wastes memory.
  • Set a sensible size limit. Use options.MaximumBodySize and options.SizeLimit to prevent the cache from exhausting server memory.
  • Monitor your hit rate. A cache that is never hit is pure overhead; instrument it and verify it earns its keep.

Common Pitfalls to Avoid

  • Forgetting to call .CacheOutput(). Registering the middleware alone caches nothing — endpoints must opt in (unless you set a base policy that applies broadly).
  • Wrong middleware order. Place UseOutputCache() after routing and CORS, before endpoints.
  • Over-varying the cache key. Varying by a unique value gives a 0% hit rate.
  • Under-varying the cache key. Forgetting to vary by a meaningful parameter serves wrong data.
  • Assuming the cache is shared. In-memory caches are per-instance; use Redis for multi-server deployments.
  • Caching error responses. By default only 200 OK responses are cached, which is what you want — don't override this without good reason.

Conclusion: Key Takeaways

ASP.NET Core output caching is a high-impact, low-effort optimization that can turn sluggish endpoints into sub-millisecond responses. By storing rendered responses on the server, it eliminates redundant database calls and serialization work, freeing your application to handle far more traffic on the same hardware.

Here are the key takeaways from this tutorial:

  • Register with AddOutputCache and enable with UseOutputCache — then opt endpoints in with .CacheOutput().
  • Prefer output caching over response caching when you need predictable, server-side, invalidatable caching.
  • Use named policies to control expiration and VaryBy rules — getting VaryBy right is the most important correctness decision.
  • Invalidate precisely with tags and EvictByTagAsync so your data stays fresh after writes.
  • Use a Redis-backed IOutputCacheStore to scale caching across multiple servers.
  • Never cache personalized data globally — it is a real security risk.

Start small: add output caching to your single busiest read endpoint, measure the latency drop, then expand from there. With a few lines of code and the best practices above, you can dramatically speed up your ASP.NET Core API responses today.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...