Rate Limiting Middleware for ASP.NET Core
Overview
Built-in rate limiting middleware (ASP.NET Core 7+) protects APIs from abuse:
- Fixed Window - Reset counter at fixed intervals
- Sliding Window - Smoothed fixed window
- Token Bucket - Allow bursts, refill over time
- Concurrency - Limit simultaneous requests
Quick Reference
| Algorithm | Best For | Characteristics |
|---|---|---|
| Fixed Window | Simple rate limits | Counter resets at interval boundary |
| Sliding Window | Smoother limiting | Weighted average across segments |
| Token Bucket | Burst tolerance | Allows bursts up to bucket size |
| Concurrency | Resource protection | Limits parallel requests |
Rate Limiting Structure
/API/RateLimiting/
├── RateLimitingConfiguration.cs
├── Policies/
│ ├── UserRateLimitPolicy.cs
│ └── IpRateLimitPolicy.cs
└── Middleware/
└── RateLimitExceededHandler.cs
Template: Basic Rate Limiting Configuration
// src/{name}.api/Program.cs
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
// ═══════════════════════════════════════════════════════════════
// RATE LIMITING CONFIGURATION
// ═══════════════════════════════════════════════════════════════
builder.Services.AddRateLimiter(options =>
{
// Global limiter settings
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
// ═══════════════════════════════════════════════════════════════
// FIXED WINDOW POLICY
// Allows N requests per time window, resets at window boundary
// ═══════════════════════════════════════════════════════════════
options.AddFixedWindowLimiter("fixed", limiterOptions =>
{
limiterOptions.PermitLimit = 100; // Requests per window
limiterOptions.Window = TimeSpan.FromMinutes(1); // Window duration
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
limiterOptions.QueueLimit = 10; // Queue when limit exceeded
});
// ═══════════════════════════════════════════════════════════════
// SLIDING WINDOW POLICY
// Smoother rate limiting with segments
// ═══════════════════════════════════════════════════════════════
options.AddSlidingWindowLimiter("sliding", limiterOptions =>
{
limiterOptions.PermitLimit = 100;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.SegmentsPerWindow = 6; // 6 segments = 10s each
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
limiterOptions.QueueLimit = 10;
});
// ═══════════════════════════════════════════════════════════════
// TOKEN BUCKET POLICY
// Allows bursts, then rate-limited
// ═══════════════════════════════════════════════════════════════
options.AddTokenBucketLimiter("token", limiterOptions =>
{
limiterOptions.TokenLimit = 100; // Maximum tokens (bucket size)
limiterOptions.ReplenishmentPeriod = TimeSpan.FromSeconds(1);
limiterOptions.TokensPerPeriod = 10; // Tokens added per period
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
limiterOptions.QueueLimit = 10;
limiterOptions.AutoReplenishment = true;
});
// ═══════════════════════════════════════════════════════════════
// CONCURRENCY LIMITER
// Limits simultaneous requests (not rate)
// ═══════════════════════════════════════════════════════════════
options.AddConcurrencyLimiter("concurrency", limiterOptions =>
{
limiterOptions.PermitLimit = 50; // Max concurrent requests
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
limiterOptions.QueueLimit = 25;
});
// Custom response for rejected requests
options.OnRejected = async (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString();
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "Too many requests",
message = "Rate limit exceeded. Please try again later.",
retryAfterSeconds = retryAfter?.TotalSeconds ?? 60
}, cancellationToken);
};
});
var app = builder.Build();
// ═══════════════════════════════════════════════════════════════
// MIDDLEWARE ORDER - Rate limiting before authentication
// ═══════════════════════════════════════════════════════════════
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
Template: Policy-Based Rate Limiting
// src/{name}.api/RateLimiting/RateLimitPolicies.cs
namespace {name}.api.ratelimiting;
public static class RateLimitPolicies
{
// Policy names
public const string Anonymous = "anonymous";
public const string Authenticated = "authenticated";
public const string Premium = "premium";
public const string Api = "api";
public const string Upload = "upload";
}
// src/{name}.api/RateLimiting/RateLimitingConfiguration.cs
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
namespace {name}.api.ratelimiting;
public static class RateLimitingConfiguration
{
public static IServiceCollection AddRateLimitingPolicies(
this IServiceCollection services,
IConfiguration configuration)
{
var settings = configuration
.GetSection("RateLimiting")
.Get<RateLimitSettings>() ?? new RateLimitSettings();
services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
// ═══════════════════════════════════════════════════════════════
// ANONYMOUS USERS - Stricter limits
// ═══════════════════════════════════════════════════════════════
options.AddFixedWindowLimiter(RateLimitPolicies.Anonymous, limiterOptions =>
{
limiterOptions.PermitLimit = settings.AnonymousRequestsPerMinute;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueLimit = 5;
});
// ═══════════════════════════════════════════════════════════════
// AUTHENTICATED USERS - More generous limits
// ═══════════════════════════════════════════════════════════════
options.AddTokenBucketLimiter(RateLimitPolicies.Authenticated, limiterOptions =>
{
limiterOptions.TokenLimit = settings.AuthenticatedBurstLimit;
limiterOptions.ReplenishmentPeriod = TimeSpan.FromSeconds(1);
limiterOptions.TokensPerPeriod = settings.AuthenticatedTokensPerSecond;
limiterOptions.QueueLimit = 20;
});
// ═══════════════════════════════════════════════════════════════
// PREMIUM USERS - Highest limits
// ═══════════════════════════════════════════════════════════════
options.AddTokenBucketLimiter(RateLimitPolicies.Premium, limiterOptions =>
{
limiterOptions.TokenLimit = settings.PremiumBurstLimit;
limiterOptions.ReplenishmentPeriod = TimeSpan.FromSeconds(1);
limiterOptions.TokensPerPeriod = settings.PremiumTokensPerSecond;
limiterOptions.QueueLimit = 50;
});
// ═══════════════════════════════════════════════════════════════
// API ENDPOINTS - Pe