The Error
HTTP 429 Too Many Requests
Microsoft.AspNetCore.RateLimiting: Request was rejected by rate limiter
Quick Fix - 2 Minutes
// Program.cs - Dev Fix: Configure rate limiting.NET 8
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", opt =>
{
opt.PermitLimit = 100; // Dev Fix: Increase limit
opt.Window = TimeSpan.FromMinutes(1);
opt.QueueLimit = 10;
});
});
var app = builder.Build();
app.UseRateLimiter(); // Must be before MapControllers
// Controller
[EnableRateLimiting("api")]
[ApiController]
public class DataController : ControllerBase { }
Why This Happens
.NET 8 rate limiting middleware rejects requests over your limit. Default FixedWindow is 100/min. Common with bots, polling, or load testing. Returns 429 with Retry-After header.
Real-World Scenario: Load Balancer Causes 429 for All Users
#1 prod incident. Rate limiting triggers for everyone because IP is wrong:
// WRONG: Rate limiter sees load balancer IP, not user IP
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString()?? "unknown",
factory: partition => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
});
// RIGHT: Use X-Forwarded-For header + OnRejected handler
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("PerUser", httpContext =>
RateLimitPartition.GetSlidingWindowLimiter(
partitionKey: GetUserKey(httpContext), // Dev Fix: User ID or IP from header
factory: _ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 1000,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 6, // Dev Fix: Smoother than FixedWindow
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 50
}));
// Dev Fix: Custom 429 response with Retry-After
options.OnRejected = async (context, token) =>
{
context.HttpContext.Response.StatusCode = 429;
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter = retryAfter.TotalSeconds.ToString();
}
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "Too many requests. Retry after " + retryAfter.TotalSeconds + "s"
}, token);
};
});
string GetUserKey(HttpContext context)
{
// Priority: JWT user ID > X-Forwarded-For > RemoteIpAddress
var userId = context.User?.FindFirst("sub")?.Value;
if (!string.IsNullOrEmpty(userId)) return $"user_{userId}";
var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
if (!string.IsNullOrEmpty(forwardedFor)) return forwardedFor.Split(',')[0].Trim();
return context.Connection.RemoteIpAddress?.ToString()?? "anonymous";
}
var app = builder.Build();
app.UseRateLimiter(); // Dev Fix: Before auth so rejected requests skip auth pipeline
app.UseAuthentication();
app.UseAuthorization();
3 critical fixes:
- X-Forwarded-For: Behind Azure/AWS/Cloudflare,
RemoteIpAddressis the LB IP. All users share it = global 429. Read real IP from header. - SlidingWindow vs FixedWindow: FixedWindow allows 100 req at 0:59 + 100 at 1:00 = 200 burst. SlidingWindow spreads it.
- Exclude health checks:
[DisableRateLimiting]on/healthor k8s kills your pod during traffic spike.
Related Fixes You Should Know
429 errors often mask these issues:
- Minimal API 400 Validation - Client retries 400 errors aggressively → triggers 429. Fix validation first.
- HttpClient Socket Exhaustion - Your API calls downstream service, gets 429, retries without backoff, exhausts sockets.
- Polly Retry + Circuit Breaker - Handle 429 from external APIs. Respect
Retry-Afterheader or get IP banned. - SignalR Negotiation 429 - SignalR negotiate endpoint hit rate limit. Users can't connect. Use
[DisableRateLimiting]on hub.
FAQ
Q: What's the difference between GlobalLimiter and per-endpoint limits?
GlobalLimiter applies to all requests. [EnableRateLimiting("policy")] applies per controller/action. Use Global for DDoS, per-endpoint for expensive operations like /reports/generate.
Q: Should I rate limit by IP or User ID?
User ID after login. IP before login. IP-only is weak: CGNAT means 1000s of users share 1 IP. Auth endpoints need IP limit to stop brute force. Use both.
Best Practice for.NET 8
- Use
SlidingWindowLimiterfor smoother limits vs bursts - Add
OnRejectedhandler to log + return custom JSON - Exclude health checks:
[DisableRateLimiting] - For auth endpoints: set lower limits to block brute force
No comments yet. Be the first to share your thoughts!