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 -> 429Remember
- 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.