Production Rate Limiting in ASP.NET Core
A production rate-limit policy should start from the resource that saturates first, not from an algorithm. Benjamin Abt shows how to layer concurrency and time-based limits, choose partition keys that are safe as well as fair, order the middleware, and judge what in-memory state can and cannot promise once the app scales out.
Key points
- Time-based quotas and concurrency limits answer different questions; production APIs usually need both.
- Partition on tenant ID, OAuth client ID or subject ID, never on API keys, Authorization headers or query strings.
- Set RejectionStatusCode explicitly: 429 for an exhausted caller quota, 503 for service-wide saturation.
- Built-in limiter state is per process, so 20 replicas at 100 requests per minute admit roughly 2,000; pick gateway, distributed or per-replica allocation and document the scope.
- Keep limiter queues at zero until service-time measurements justify a maximum acceptable queue delay.
- In .NET 11 Preview 7 the FixedWindowRateLimiter Retry-After metadata points to the next window boundary.
Rate limiting in ASP.NET Core usually stops at a fixed window registration, which says nothing about what actually saturates a service. Benjamin Abt starts from the resource that fails first — database connections, CPU, a downstream API — and separates two questions: how much work a client may submit over time, and how much may run at once. Time-based policies are paired with a concurrency limiter, since either one alone lets slow requests or fast bursts through.
The partition key is treated as a security boundary: tenant ID, OAuth client ID or subject ID, with a normalized remote IP as an anonymous fallback, never API keys or headers. Middleware order puts forwarded headers, routing and authentication first, in-memory state is scoped per process, and 429 is distinguished from 503.
Real-world use
- A multi-tenant SaaS API that needs a per-tenant quota plus a tighter concurrency ceiling on report and export endpoints.
- A public API behind a CDN and WAF where anonymous callers fall back to normalized remote IP partitions while authenticated machines use OAuth client IDs.
- A service calling a paid third-party API with a commercial quota, where weighted cost needs durable business accounting instead of an in-memory middleware counter.
- A containerized API on many replicas that has to choose between a gateway quota, atomic state in Redis and a documented per-replica allocation.
Why it matters
A policy copied from a tutorial lets slow endpoints or bursty callers through and hides real capacity problems. Treating partitions, status codes and quota scope as explicit design decisions keeps the protection correct as replicas and tenants grow.