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:

Example.cs
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:

Example.cs
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:

Example.cs
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:

Example.cs
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:

Example.cs
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:

Example.cs
// 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:

Example.cs
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:

Example.cs
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.