Skip to content

Case 16: Caching at the Edge - Response Caching

The Story

Imagine you have a public API endpoint, /api/news/latest, which fetches the 10 most recent news articles. This data is the same for all users and only needs to be updated every 2 minutes.

Currently, even if the underlying service uses IMemoryCache to get the data quickly, every single request to this endpoint still has to go through the entire ASP.NET Core pipeline (routing, creating the controller, running the action method, and serializing the result to JSON).

The Problem Code (No Response Caching)

csharp
[ApiController]
[Route("api/news")]
public class NewsController : ControllerBase
{
    private readonly INewsService _newsService;

    public NewsController(INewsService newsService)
    {
        _newsService = newsService;
    }

    [HttpGet("latest")]
    public async Task<IActionResult> GetLatestNews()
    {
        // Even if the service is cached, this code runs for EVERY request
        var latestNews = await _newsService.GetLatestNewsAsync();
        return Ok(latestNews);
    }
}

Why is this a problem?

  1. Wasted Server Work: Even if _newsService.GetLatestNewsAsync() returns data from IMemoryCache in just 1 millisecond, your server still has to spend CPU resources to:
    • Process the HTTP request.
    • Run the routing logic.
    • Create a new NewsController instance.
    • Call the GetLatestNews method.
    • Convert (serialize) the C# latestNews object into a JSON string.
  2. The Core Issue: The application is repeating the entire request-handling process over and over again just to produce the exact same result for everyone.

The Solution: Use Response Caching Middleware

Response Caching works at the HTTP level. The middleware intercepts an incoming request. If it finds a valid cached response for that URL, it sends the cached response back immediately without running the rest of the pipeline. The request never even reaches the controller.

Here is the better code:

  1. Register it in Program.cs:
    csharp
    builder.Services.AddResponseCaching();
    // ...
    var app = builder.Build();
    // ...
    app.UseResponseCaching(); // Add the middleware to the pipeline
    app.UseRouting();
    // ...
  2. Add an Attribute to the Action:
    csharp
    [HttpGet("latest")]
    // Instructs the middleware to cache the response of this action for 120 seconds (2 minutes)
    [ResponseCache(Duration = 120, Location = ResponseCacheLocation.Any)]
    public async Task<IActionResult> GetLatestNews()
    {
        // This logic now only runs for the first request every 2 minutes
        var latestNews = await _newsService.GetLatestNewsAsync();
        return Ok(latestNews);
    }

What We Gained

  1. The First Request: The request goes through the pipeline as normal, and the controller action is executed. Before sending the response to the client, the ResponseCachingMiddleware saves a copy of it in memory.
  2. Subsequent Requests (within 2 minutes): The ResponseCachingMiddleware intercepts the request, finds the cached copy, and sends it back immediately. The Controller and Service are never executed.
  3. Performance:
    • Maximum Server Load Reduction: The server uses almost no CPU for cached requests.
    • Ultra-Fast Responses: Response time is minimal because it's just a memory lookup.
    • Leverages Client/Proxy Caching: Location = ResponseCacheLocation.Any allows the user's browser and intermediate proxies to also cache the response, meaning later requests might not even need to travel to your server at all.

Comparing IMemoryCache and Response Caching

CriteriaIMemoryCache (Data Caching)Response Caching (Output Caching)
PurposeCaches data inside the applicationCaches the entire HTTP response
What is CachedC# objectsJSON, HTML (as bytes)
LocationServer-side onlyServer, Proxy, Client (browser)
Best ForData that is reused in many placesPublic endpoints with the same result for everyone

Conclusion

  • Response Caching is a high-level caching strategy that is extremely effective for public endpoints with data that changes infrequently and is the same for all users.
  • It does not replace IMemoryCache but complements it. You can use both to create a powerful, multi-layered caching strategy.