Caching and rate limiting

ASP.NET Core · Advanced · 14 min

Caching cuts latency and database load; rate limits protect your service from abuse and overload.

Output caching stores whole responses on the server. Register AddOutputCache(), add app.UseOutputCache(), and mark endpoints with .CacheOutput(p => p.Expire(TimeSpan.FromSeconds(30))) or named policies. Vary by query, header or route values, and evict by tag (IOutputCacheStore.EvictByTagAsync("todos")) when data changes. Only GET/HEAD 200 responses for anonymous requests are cached by default.

Data caching caches values inside your code:

  • IMemoryCache: in-process, fast, lost on restart, not shared between instances.
  • IDistributedCache (Redis, SQL Server): shared, but you serialize bytes yourself.
  • HybridCache (Microsoft.Extensions.Caching.Hybrid, .NET 9+): combines an in-memory layer with an optional distributed one, handles serialization, and prevents cache stampedes: concurrent misses for one key run the factory only once. await cache.GetOrCreateAsync(key, async ct => await LoadAsync(ct)).

Rate limiting (AddRateLimiter + app.UseRateLimiter()) rejects excess requests, by default with 503; set RejectionStatusCode = 429. Algorithms: fixed window, sliding window, token bucket and concurrency limiters. Apply them with .RequireRateLimiting("policy") and partition by user or client IP so one client can't exhaust others' quota.

In-process limits apply per instance; behind a load balancer with many instances, enforce global limits at the gateway too.

Example

using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
builder.Services.AddRateLimiter(o =>
{
    o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    o.AddPolicy("per-ip", ctx => RateLimitPartition.GetFixedWindowLimiter(
        ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        _ => new FixedWindowRateLimiterOptions { PermitLimit = 3, Window = TimeSpan.FromSeconds(10) }));
});
var app = builder.Build();
app.UseRateLimiter();
app.UseOutputCache();

app.MapGet("/time", () => DateTime.UtcNow.ToString("O"))
   .CacheOutput(p => p.Expire(TimeSpan.FromSeconds(5)).Tag("time"));
app.MapGet("/search", (string q) => $"results for {q}")
   .RequireRateLimiting("per-ip");
app.Run();
// GET /time twice within 5 s -> same timestamp (served from cache)
// 4th GET /search?q=x within 10 s -> 429

Remember

  • Output caching stores full responses; evict by tag on changes
  • HybridCache adds two-level caching and stampede protection
  • Rate limiters reject excess load; set 429 as the status code
  • Partition limits per user or IP

Watch out for

  • Output-caching endpoints whose responses depend on the authenticated user.

Check yourself

  1. What status does the rate limiting middleware return for rejected requests unless configured otherwise?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/aspnet/core/performance/rate-limit

Builds on

Unlocks

Nothing builds on this yet.