IP Rate Limiting in ASP.NET Core (.NET 8): Return 429 Too Many Requests
Limit requests per IP using .NET 8’s built-in rate limiting middleware and return 429 with Retry-After to protect your API from abuse.
Public APIs often need protection against abuse, accidental request floods, or simple brute-force patterns.
One of the simplest defenses is rate limiting: you allow only a certain number of requests in a time window,
and return 429 Too Many Requests when the client exceeds the limit.
In .NET 8, you can implement this in a modern way using the built-in Rate Limiting middleware.
In this example, we apply a per-IP limit and include a Retry-After header to tell clients when to try again.
Goal
- Limit requests per IP address.
- Return
429 Too Many Requestswhen the limit is exceeded. - Include
Retry-Afterto help clients back off gracefully. - Keep the setup simple and easy to reuse across endpoints.
Program.cs (Fixed Window Rate Limiting per IP)
This configuration allows 60 requests per minute per IP.
When the limit is exceeded, the middleware returns 429 and we add a Retry-After header.
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddRateLimiter(options =>
{
// Default status code for rejected requests
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
// Policy name: "ip"
options.AddPolicy("ip", httpContext =>
{
var ip = GetClientIp(httpContext);
// Partition by IP. Each IP gets its own limiter instance.
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: ip,
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 60, // 60 requests
Window = TimeSpan.FromMinutes(1), // per 1 minute
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0 // no queue: reject immediately
});
});
// Add Retry-After header on rejection
options.OnRejected = async (context, ct) =>
{
context.HttpContext.Response.Headers["Retry-After"] = "60";
await context.HttpContext.Response.WriteAsync("Too many requests.", ct);
};
});
var app = builder.Build();
app.UseRateLimiter();
app.MapControllers()
.RequireRateLimiting("ip");
app.Run();
static string GetClientIp(HttpContext httpContext)
{
// If you are behind a reverse proxy, see the "Proxy/CORS note" section below.
return httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}
Where to Apply the Policy
In the example above, we applied rate limiting to all controllers via MapControllers().RequireRateLimiting("ip").
If you want to apply it only to specific endpoints, you can use attributes or per-endpoint configuration instead.
Testing Quickly
You can simulate a burst of requests (Linux/macOS) like this:
for i in {1..80}; do
curl -s -o /dev/null -w "%{http_code}\n" https://localhost:5001/api/customers
done
After the 60th request (within a minute), you should see 429 responses.
Important Note: Reverse Proxy and Real Client IP
If your API is behind a reverse proxy (NGINX, Cloudflare, Azure App Gateway, etc.), RemoteIpAddress may contain
the proxy’s IP instead of the real client IP. In that case, per-IP rate limiting won’t work correctly unless you
configure forwarded headers.
In ASP.NET Core, you can enable forwarded headers (only trust known proxies!):
using Microsoft.AspNetCore.HttpOverrides;
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// IMPORTANT: Add known proxy networks/IPs in production
// options.KnownProxies.Add(IPAddress.Parse("10.0.0.10"));
});
var app = builder.Build();
app.UseForwardedHeaders();
Once forwarded headers are configured correctly, RemoteIpAddress will reflect the actual client IP.
Common Improvements
- Use different limits per endpoint (e.g., tighter limits for login).
- Whitelist internal IPs or exclude health checks.
- Use a distributed store if you run multiple instances (in-memory limits are per instance).
- Return a structured error (e.g., ProblemDetails) instead of plain text.
TL;DR
- IP-based rate limiting is a simple way to protect APIs from bursts and abuse.
- .NET 8 provides built-in rate limiting via
AddRateLimiter()+UseRateLimiter(). - On overflow, return
429and includeRetry-Afterto guide clients. - Behind a proxy, configure forwarded headers to get the real client IP.