Isaac.

HTTP Caching Strategies

Leverage HTTP caching to improve performance and reduce bandwidth.

By EMEPublished: February 20, 2025
httpcachingperformancecdnheaders

A Simple Analogy

HTTP caching is like keeping frequently used items on your desk. You don't reacquire them from storage; you just grab them.


Why Caching?

  • Speed: Instant responses
  • Bandwidth: Reduce network traffic
  • Server load: Fewer requests
  • User experience: Faster page loads
  • Reliability: Works offline

Cache Headers

// Set cache policy
app.MapGet("/api/products/{id}", async (int id) => 
{
    var product = await GetProductAsync(id);
    return Results.Ok(product);
})
.WithName("GetProduct")
.WithOpenApi()
.CacheOutput(policy => policy
    .Expire(TimeSpan.FromHours(1))
    .Tag("products")
);

// Or manual headers
app.MapGet("/static/file", () =>
{
    return Results.File("file.txt", "text/plain")
        .WithHeaders(h => 
        {
            h.CacheControl = "public, max-age=3600";
            h.ETag = "\"abc123\"";
            h.Expires = DateTimeOffset.UtcNow.AddHours(1);
        });
});

Cache-Control Directives

// Public cache (CDNs and browsers)
Cache-Control: public, max-age=3600

// Private cache (browser only)
Cache-Control: private, max-age=3600

// No caching
Cache-Control: no-cache, no-store

// Revalidate before using
Cache-Control: public, max-age=3600, must-revalidate

Example in code:

response.Headers.CacheControl = "public, max-age=3600, must-revalidate";

ETags and Validation

app.MapGet("/api/data", async (HttpContext context) =>
{
    var data = await GetDataAsync();
    var etag = "\"data-v123\"";
    
    // Client has fresh copy?
    if (context.Request.Headers.IfNoneMatch == etag)
    {
        return Results.StatusCode(304);  // Not Modified
    }
    
    context.Response.Headers.ETag = etag;
    context.Response.Headers.CacheControl = "max-age=3600";
    
    return Results.Ok(data);
});

Conditional Requests

// Last-Modified
app.MapGet("/file", async (HttpContext context) =>
{
    var lastModified = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
    
    if (context.Request.Headers.IfModifiedSince.Count > 0)
    {
        var clientTime = DateTimeOffset.Parse(context.Request.Headers.IfModifiedSince);
        if (clientTime >= lastModified)
        {
            return Results.StatusCode(304);  // Not Modified
        }
    }
    
    var file = await GetFileAsync();
    context.Response.Headers.LastModified = lastModified.ToString("r");
    context.Response.Headers.CacheControl = "max-age=86400";
    
    return Results.File(file, "application/octet-stream");
});

Distributed Caching

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
});

app.MapGet("/api/expensive", async (
    IDistributedCache cache,
    HttpContext context) =>
{
    const string cacheKey = "expensive-data";
    
    // Try cache first
    var cachedData = await cache.GetAsync(cacheKey);
    if (cachedData != null)
    {
        return Results.Ok(System.Text.Encoding.UTF8.GetString(cachedData));
    }
    
    // Compute if not cached
    var data = await ExpensiveComputationAsync();
    var bytes = System.Text.Encoding.UTF8.GetBytes(data);
    
    // Cache for 1 hour
    await cache.SetAsync(cacheKey, bytes, 
        new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
        });
    
    return Results.Ok(data);
});

Best Practices

  1. Version assets: Use query params or filenames
  2. Cache busting: Change URLs for new content
  3. Validation: Use ETags and Last-Modified
  4. Security: Cache-Control for sensitive data
  5. Monitoring: Track cache hit rates

Related Concepts

  • CDN strategies
  • Service workers
  • Cache invalidation
  • Browser storage (localStorage, sessionStorage)

Summary

HTTP caching dramatically improves performance through intelligent reuse of responses. Use Cache-Control headers, ETags, and distributed caches for optimal results.