.NET
Implementing Rate Limiting in ASP.NET Core APIs
Learn how to protect ASP.NET Core APIs with the built-in rate-limiting middleware, named policies, client partitions, HTTP 429 responses, and practical configuration choices.
Rate limiting controls how many requests an API accepts during a specific period. It is useful for public APIs, authentication endpoints, expensive operations, and integrations where one client should not consume all available resources.
ASP.NET Core provides built-in rate-limiting middleware with fixed window, sliding window, token bucket, and concurrency algorithms. Policies can be global or assigned only to selected endpoints. (learn.microsoft.com)
Why rate limiting matters
A rate limiter can help an API:
- prevent a single client from monopolizing resources;
- reduce accidental traffic spikes;
- protect database, CPU, and external-service capacity;
- enforce different limits for different users or plans;
- return a predictable response when capacity is unavailable.
Rate limiting is not complete DDoS protection. Large distributed attacks should also be handled at infrastructure boundaries such as a CDN, web application firewall, API gateway, or cloud protection service. (learn.microsoft.com)
Configuring a fixed-window policy
The fixed-window algorithm permits a defined number of requests during a time window. When the window ends, its counter is reset.
The following Program.cs configuration accepts 20 requests every minute and queues up to two additional requests:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddFixedWindowLimiter("api", limiterOptions =>
{
limiterOptions.PermitLimit = 20;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueLimit = 2;
limiterOptions.QueueProcessingOrder =
QueueProcessingOrder.OldestFirst;
});
});
var app = builder.Build();
app.UseRouting();
app.UseRateLimiter();
app.MapGet("/products", () => Results.Ok(new[]
{
new { Id = 1, Name = "Keyboard" },
new { Id = 2, Name = "Mouse" }
})).RequireRateLimiting("api");
app.Run();
PermitLimit defines how many requests are accepted, while Window defines when the permits are replenished. QueueLimit controls how many requests can wait for a permit. Setting it to zero rejects excess requests immediately.
For endpoint-specific policies, UseRateLimiter should run after UseRouting. (learn.microsoft.com)
Returning a useful 429 response
By default, rejected requests can receive HTTP status code 429 Too Many Requests. An OnRejected callback allows the API to return a clearer response and, when available, a Retry-After header.
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.OnRejected = async (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(
MetadataName.RetryAfter,
out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString();
}
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "Too many requests. Please try again later."
}, cancellationToken);
};
options.AddFixedWindowLimiter("api", limiterOptions =>
{
limiterOptions.PermitLimit = 20;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueLimit = 0;
});
});
The Retry-After value helps clients decide when another request is likely to succeed. It is still important for clients to handle 429 responses instead of assuming every request will be accepted.
Limiting each client separately
A single shared policy means every caller consumes the same request quota. Partitioning creates a separate limiter for each user, API key, tenant, or IP address.
options.AddPolicy("per-client", httpContext =>
{
var clientId =
httpContext.User.Identity?.Name ??
httpContext.Connection.RemoteIpAddress?.ToString() ??
"anonymous";
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: clientId,
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 10,
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0,
AutoReplenishment = true
});
});
Apply it like any other named policy:
app.MapPost("/orders", CreateOrder)
.RequireRateLimiting("per-client");
An authenticated user ID or controlled API key is generally a more reliable partition key than an IP address. Applications behind proxies must also configure forwarded headers correctly before depending on the client IP.
Choosing an algorithm
Use fixed window when a simple quota is enough. Use sliding window for smoother limits across window boundaries. Use token bucket when controlled bursts are acceptable. Use concurrency limiting when the main concern is how many expensive operations run simultaneously rather than how many requests arrive per minute. (learn.microsoft.com)
Before production deployment, keep limits in configuration, monitor rejected requests, and load-test the selected values. A limit should reflect the actual cost and capacity of the protected endpoint—not an arbitrary number copied across the entire API.
References
- Rate limiting middleware in ASP.NET Core | Microsoft Learn — learn.microsoft.com
- Dotnet Api Rate Limiting Guide 058F3a700dcb — medium.com
- Http Ratelimiter — learn.microsoft.com
- 31 Rate Limiting In Netcore — adrianbailador.github.io
- Implement Rate Limiting In Asp Net Core Web Api — c-sharpcorner.com