Microsoft.AspNetCore.RateLimiting lives in the Web block (BuildingBlocks/Web/RateLimiting/). The kit ships two layers:
- A global limiter — three chained fixed-window limiters partitioned per tenant, per user, and per IP — applied to every request when rate limiting is enabled.
- A named
authpolicy that throttles authentication-flow endpoints — login, refresh, forgot-password, reset-password, confirm-email, resend-confirmation, self-register — for brute-force protection where it matters most.
How it’s configured
Everything binds from the RateLimitingOptions section. These are the shipped values (also the code defaults):
{ "RateLimitingOptions": { "Enabled": false, // false in appsettings.json (dev); true in appsettings.Production.json "Tenant": { "PermitLimit": 1000, "WindowSeconds": 60, "QueueLimit": 0 }, "User": { "PermitLimit": 200, "WindowSeconds": 60, "QueueLimit": 0 }, "Ip": { "PermitLimit": 300, "WindowSeconds": 60, "QueueLimit": 0 }, "Auth": { "PermitLimit": 10, "WindowSeconds": 60, "QueueLimit": 0 } }}All four buckets are fixed-window limiters with no queueing (QueueLimit: 0) — excess requests get an immediate 429. Rate limiting is off in local dev (appsettings.json) and on in production (appsettings.Production.json).
The global limiter
When enabled, AddHeroRateLimiting builds a chained GlobalLimiter (PartitionedRateLimiter.CreateChained) — a request must pass all three dimensions:
| Dimension | Partition key | Skipped when |
|---|---|---|
| Tenant | tenant:{tenant claim} | No tenant claim (anonymous) |
| User | user:{NameIdentifier claim} | Not authenticated |
| IP | ip:{RemoteIpAddress} | No resolvable remote IP |
Health probe paths (/health, /healthz, /ready, /live) are exempt from all three, so aggressive orchestrator probes never get throttled. The kit’s health endpoints also call .DisableRateLimiting() on their route group.
The tenant bucket is the SaaS guardrail: one noisy tenant burns its own 1000-requests-per-minute budget without starving the others.
The auth policy
The auth policy partitions by user id when authenticated, otherwise by IP, at 10 requests per minute. The Identity module attaches it with .RequireRateLimiting("auth"):
| Endpoint | Why |
|---|---|
POST /api/v1/identity/token/issue | Login attempts |
POST /api/v1/identity/token/refresh | Token refresh attempts |
POST /api/v1/identity/forgot-password | Password reset request abuse |
POST /api/v1/identity/reset-password | Password reset attempts |
GET /api/v1/identity/confirm-email | Email confirmation attempts |
POST /api/v1/identity/users/{id}/resend-confirmation-email | Confirmation mail flooding |
POST /api/v1/identity/self-register | Self-registration abuse |
group.MapGenerateTokenEndpoint().AllowAnonymous().RequireRateLimiting("auth");AuthRateLimitWiringTests (in src/Tests/Integration.Tests/Tests/Authentication/) verifies every endpoint that should carry the policy actually does. Run it after touching the Identity module’s endpoint registrations.
Reuse the same policy on your own sensitive anonymous endpoints — anything a bot can hammer without credentials is a candidate:
endpoints.MapPost("/api/v1/contact", handler) .AllowAnonymous() .RequireRateLimiting("auth");Additional named policies aren’t config-driven — they’re registered in code inside AddHeroRateLimiting (the Web building block, which is protected; see buildingblocks-protection.md before changing it).
What a rejected request sees
The limiter rejects with HTTP 429 and a ProblemDetails body (application/problem+json):
HTTP/1.1 429 Too Many RequestsRetry-After: 42Content-Type: application/problem+json
{ "type": "https://datatracker.ietf.org/doc/html/rfc6585#section-4", "title": "Too Many Requests", "status": 429, "detail": "Rate limit exceeded. Please retry later.", "instance": "/api/v1/identity/token/issue", "traceId": "4a7d8e1f2c...", "correlationId": "..."}Retry-After (seconds) is included when the limiter can compute it; traceId and correlationId cross-reference server logs and traces.
What rate limiting doesn’t protect against
- Distributed brute force — 10,000 IPs sending one request each per minute get through every per-IP limit. Combine with credential-stuffing detection, MFA, and account lockout (the kit ships lockout via ASP.NET Identity — 15-minute default lockout window).
- Slow-rate attacks — an attacker sending 1 request per 10 seconds from one IP never hits a 10/min limit but might still be malicious. Combine with anomaly detection on auth-failure rates.
- Application-layer DDoS — rate limiting helps but isn’t a complete defence. Use a CDN / WAF in front (Cloudflare, AWS WAF, Azure Front Door) for layer-7 protection.
Watching for false positives
When a legitimate user hits the limit, they see 429 + Retry-After. In production, log every 429 with the source partition and endpoint:
- A single IP hitting 429 repeatedly on login → likely brute force.
- Many users hitting 429 on the same legitimate endpoint → your limit is too low.
- One user hitting 429 on refresh → their client may be misconfigured (no token caching, retrying too aggressively).
Set up an alert on “429 rate > X% of requests” so you catch tuning issues early.
Related
- Security overview — broader auth + abuse protection.
- Quota building block — per-tenant resource budgets (different concern).
- HTTP resilience — caller-side back-off when receiving 429.