Rate Limiting Middleware in ASP.NET Core
Rate limiting protects your API from abuse, prevents resource exhaustion, and ensures fair usage across clients. .NET 7 introduced built-in rate limiting middleware, removing the need for third-party packages.
Getting Started
Add the rate limiting services and middleware:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
var app = builder.Build();
app.UseRateLimiter();
app.MapGet("/api/products", GetProducts)
.RequireRateLimiting("fixed");
app.Run();
The Four Algorithms
ASP.NET Core provides four rate limiting algorithms, each suited to different scenarios.
Fixed Window
Allows a fixed number of requests within a time window. Simple but can allow bursts at the window boundary:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", config =>
{
config.PermitLimit = 10;
config.Window = TimeSpan.FromMinutes(1);
config.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
config.QueueLimit = 5;
});
});
With this configuration, each client gets 10 requests per minute. If the limit is hit, up to 5 additional requests are queued rather than rejected immediately.
Sliding Window
Divides the window into segments and distributes permits more evenly, reducing the boundary burst problem:
builder.Services.AddRateLimiter(options =>
{
options.AddSlidingWindowLimiter("sliding", config =>
{
config.PermitLimit = 30;
config.Window = TimeSpan.FromMinutes(1);
config.SegmentsPerWindow = 6; // 10-second segments
config.QueueLimit = 0;
});
});
Token Bucket
Tokens replenish at a steady rate. Allows short bursts up to the bucket capacity while maintaining a consistent average rate:
builder.Services.AddRateLimiter(options =>
{
options.AddTokenBucketLimiter("token", config =>
{
config.TokenLimit = 20; // Maximum burst size
config.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
config.TokensPerPeriod = 5; // 5 tokens every 10 seconds
config.AutoReplenishment = true;
config.QueueLimit = 0;
});
});
This allows a burst of 20 requests, then sustains a rate of roughly 30 requests per minute.
Concurrency Limiter
Limits the number of concurrent requests rather than the rate:
builder.Services.AddRateLimiter(options =>
{
options.AddConcurrencyLimiter("concurrent", config =>
{
config.PermitLimit = 5; // Max 5 simultaneous requests
config.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
config.QueueLimit = 10;
});
});
This is ideal for protecting resource-intensive endpoints like report generation or file processing.
Applying Rate Limits
Apply rate limiting to specific endpoints or groups:
// Single endpoint
app.MapGet("/api/search", Search)
.RequireRateLimiting("sliding");
// Route group
var api = app.MapGroup("/api")
.RequireRateLimiting("fixed");
api.MapGet("/products", GetProducts);
api.MapGet("/orders", GetOrders);
// Controller attribute
[EnableRateLimiting("token")]
[ApiController]
[Route("api/[controller]")]
public class ReportsController : ControllerBase
{
[HttpGet]
public IActionResult GetReports() => Ok();
[DisableRateLimiting] // Exempt this action
[HttpGet("health")]
public IActionResult Health() => Ok("healthy");
}
Per-Client Rate Limiting
The default limiters apply globally. For per-client limiting, use a partitioned rate limiter:
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("per-user", httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name
?? httpContext.Connection.RemoteIpAddress?.ToString()
?? "anonymous",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
});
Each unique user (or IP address for anonymous requests) gets their own rate limit counter.
Customising the Rejection Response
By default, rate-limited requests receive a bare 429 status code. Provide a more helpful response:
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = 429;
options.OnRejected = async (context, cancellationToken) =>
{
context.HttpContext.Response.ContentType = "application/json";
var retryAfter = context.Lease.TryGetMetadata(
MetadataName.RetryAfter, out var retryAfterValue)
? retryAfterValue.TotalSeconds
: 60;
context.HttpContext.Response.Headers.RetryAfter =
retryAfter.ToString();
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "Too many requests. Please try again later.",
retryAfterSeconds = retryAfter
}, cancellationToken);
};
});
The Retry-After header tells well-behaved clients how long to wait before retrying.
Choosing the Right Algorithm
| Algorithm | Best for | Burst handling |
|---|---|---|
| Fixed Window | Simple rate limits | Allows boundary bursts |
| Sliding Window | Smoother distribution | Reduces bursts |
| Token Bucket | APIs with occasional spikes | Controlled bursts |
| Concurrency | Resource-heavy operations | Limits parallelism |
Key Takeaways
The built-in rate limiting middleware eliminates the need for third-party solutions for most use cases. Choose the algorithm that matches your traffic pattern, use partitioned limiters for per-client fairness, and always return a meaningful Retry-After header so clients can back off gracefully.