Fix: ASP.NET Core Rate Limiting 429 Error - Dev Fix in 30 Seconds

Published: Jun 04, 2026 · By Kumar Kunal

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:

  1. X-Forwarded-For: Behind Azure/AWS/Cloudflare, RemoteIpAddress is the LB IP. All users share it = global 429. Read real IP from header.
  2. SlidingWindow vs FixedWindow: FixedWindow allows 100 req at 0:59 + 100 at 1:00 = 200 burst. SlidingWindow spreads it.
  3. Exclude health checks: [DisableRateLimiting] on /health or k8s kills your pod during traffic spike.

Related Fixes You Should Know

429 errors often mask these issues:

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

  1. Use SlidingWindowLimiter for smoother limits vs bursts
  2. Add OnRejected handler to log + return custom JSON
  3. Exclude health checks: [DisableRateLimiting]
  4. For auth endpoints: set lower limits to block brute force

Related Dev Fixes

Found this helpful?

Master C# with our complete course. Real apps, real skills, job-ready in 2 hours.

Share this fix: Twitter LinkedIn

Comments on Fix: ASP.NET Core Rate Limiting 429 Error - Dev Fix in 30 Seconds (0)

No comments yet. Be the first to share your thoughts!