24 dotnet rate limiting
Skill ronnythedev/dotnet-clean-architecture-skills/skills/24-dotnet-rate-limiting
27 AI-ready skills for generating production-grade .NET code — Clean Architecture, CQRS, DDD. Works with Claude Code, GitHub Copilot, and Cursor.
npx -y skills add ronnythedev/dotnet-clean-architecture-skills --skill 24-dotnet-rate-limitingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
What its author says it does
Copied from the file, not written here
Implements ASP.NET Core rate limiting middleware for API protection. Covers fixed window, sliding window, token bucket, and concurrency limiters with custom policies.
SKILL.md
30.8 KB, as published. Nobody here has run it
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 - Per-endpoint limiting
// ═══════════════════════════════════════════════════════════════
options.AddSlidingWindowLimiter(RateLimitPolicies.Api, limiterOptions =>
{
limiterOptions.PermitLimit = settings.ApiRequestsPerMinute;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.SegmentsPerWindow = 6;
limiterOptions.QueueLimit = 10;
});
// ═══════════════════════════════════════════════════════════════
// FILE UPLOADS - Concurrency limited
// ═══════════════════════════════════════════════════════════════
options.AddConcurrencyLimiter(RateLimitPolicies.Upload, limiterOptions =>
{
limiterOptions.PermitLimit = settings.MaxConcurrentUploads;
limiterOptions.QueueLimit = settings.UploadQueueSize;
});
// Global rejection handler
options.OnRejected = HandleRejection;
});
return services;
}
private static async ValueTask HandleRejection(
OnRejectedContext context,
CancellationToken cancellationToken)
{
var response = context.HttpContext.Response;
// Set retry-after header if available
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString();
}
// Log the rejection
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<RateLimitingConfiguration>>();
var clientIp = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var path = context.HttpContext.Request.Path;
logger.LogWarning(
"Rate limit exceeded for client {ClientIp} on path {Path}",
clientIp,
path);
response.StatusCode = StatusCodes.Status429TooManyRequests;
response.ContentType = "application/json";
await response.WriteAsJsonAsync(new
{
type = "https://tools.ietf.org/html/rfc6585#section-4",
title = "Too Many Requests",
status = 429,
detail = "Rate limit exceeded. Please slow down your requests.",
retryAfter = retryAfter?.TotalSeconds ?? 60
}, cancellationToken);
}
}
Settings Class
// src/{name}.api/RateLimiting/RateLimitSettings.cs
namespace {name}.api.ratelimiting;
public sealed class RateLimitSettings
{
public int AnonymousRequestsPerMinute { get; set; } = 30;
public int AuthenticatedBurstLimit { get; set; } = 100;
public int AuthenticatedTokensPerSecond { get; set; } = 10;
public int PremiumBurstLimit { get; set; } = 500;
public int PremiumTokensPerSecond { get; set; } = 50;
public int ApiRequestsPerMinute { get; set; } = 60;
public int MaxConcurrentUploads { get; set; } = 5;
public int UploadQueueSize { get; set; } = 10;
}
appsettings.json
{
"RateLimiting": {
"AnonymousRequestsPerMinute": 30,
"AuthenticatedBurstLimit": 100,
"AuthenticatedTokensPerSecond": 10,
"PremiumBurstLimit": 500,
"PremiumTokensPerSecond": 50,
"ApiRequestsPerMinute": 60,
"MaxConcurrentUploads": 5,
"UploadQueueSize": 10
}
}
Template: User-Based Rate Limiting (Partitioned)
// src/{name}.api/Program.cs
builder.Services.AddRateLimiter(options =>
{
// ═══════════════════════════════════════════════════════════════
// PARTITIONED BY USER ID
// Each user has their own rate limit bucket
// ═══════════════════════════════════════════════════════════════
options.AddPolicy("per-user", httpContext =>
{
var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrEmpty(userId))
{
// Anonymous users - stricter limit, partition by IP
var clientIp = httpContext.Connection.RemoteIpAddress?.ToString() ?? "anonymous";
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: clientIp,
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 30,
Window = TimeSpan.FromMinutes(1)
});
}
// Authenticated users - more generous
return RateLimitPartition.GetTokenBucketLimiter(
partitionKey: userId,
factory: _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 100,
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
TokensPerPeriod = 10
});
});
// ═══════════════════════════════════════════════════════════════
// PARTITIONED BY SUBSCRIPTION TIER
// Different limits based on user's subscription
// ═══════════════════════════════════════════════════════════════
options.AddPolicy("tier-based", httpContext =>
{
var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous";
var tier = httpContext.User.FindFirstValue("subscription_tier") ?? "free";
return tier switch
{
"enterprise" => RateLimitPartition.GetNoLimiter(userId),
"premium" => RateLimitPartition.GetTokenBucketLimiter(
partitionKey: userId,
factory: _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 500,
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
TokensPerPeriod = 50
}),
"basic" => RateLimitPartition.GetTokenBucketLimiter(
partitionKey: userId,
factory: _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 100,
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
TokensPerPeriod = 10
}),
_ => RateLimitPartition.GetFixedWindowLimiter(
partitionKey: userId,
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 20,
Window = TimeSpan.FromMinutes(1)
})
};
});
});
Template: Applying Rate Limits to Endpoints
Minimal API Endpoints
// src/{name}.api/Endpoints/OrderEndpoints.cs
public static class OrderEndpoints
{
public static void MapOrderEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/orders")
.WithTags("Orders")
.RequireAuthorization();
// ═══════════════════════════════════════════════════════════════
// APPLY RATE LIMIT TO SINGLE ENDPOINT
// ═══════════════════════════════════════════════════════════════
group.MapGet("/", GetOrders)
.RequireRateLimiting(RateLimitPolicies.Api);
group.MapGet("/{id:guid}", GetOrderById)
.RequireRateLimiting(RateLimitPolicies.Api);
// ═══════════════════════════════════════════════════════════════
// STRICTER LIMIT FOR WRITE OPERATIONS
// ═══════════════════════════════════════════════════════════════
group.MapPost("/", CreateOrder)
.RequireRateLimiting(RateLimitPolicies.Authenticated);
// ═══════════════════════════════════════════════════════════════
// DISABLE RATE LIMITING FOR SPECIFIC ENDPOINT
// ═══════════════════════════════════════════════════════════════
group.MapGet("/health", () => Results.Ok())
.DisableRateLimiting();
}
}
Controllers
// src/{name}.api/Controllers/OrdersController.cs
using Microsoft.AspNetCore.RateLimiting;
namespace {name}.api.controllers;
[ApiController]
[Route("api/[controller]")]
[EnableRateLimiting(RateLimitPolicies.Api)] // Apply to all actions
public sealed class OrdersController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetOrders()
{
// Uses controller-level rate limit
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetOrderById(Guid id)
{
// Uses controller-level rate limit
}
[HttpPost]
[EnableRateLimiting(RateLimitPolicies.Authenticated)] // Override for this action
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
{
// Uses action-level rate limit
}
[HttpGet("export")]
[EnableRateLimiting(RateLimitPolicies.Premium)] // Expensive operation
public async Task<IActionResult> ExportOrders()
{
// Uses premium rate limit
}
[HttpGet("health")]
[DisableRateLimiting] // Health checks shouldn't be rate limited
public IActionResult Health()
{
return Ok();
}
}
Group-Level Rate Limiting
// src/{name}.api/Endpoints/PublicApiEndpoints.cs
public static void MapPublicApiEndpoints(this IEndpointRouteBuilder app)
{
// Apply rate limit to entire group
var group = app.MapGroup("/api/public")
.RequireRateLimiting(RateLimitPolicies.Anonymous);
group.MapGet("/products", GetProducts);
group.MapGet("/categories", GetCategories);
}
Template: Distributed Rate Limiting with Redis
// src/{name}.api/RateLimiting/RedisRateLimitingConfiguration.cs
using System.Threading.RateLimiting;
using StackExchange.Redis;
namespace {name}.api.ratelimiting;
/// <summary>
/// For distributed systems, implement custom rate limiter with Redis
/// to share rate limit state across multiple instances.
/// </summary>
public static class RedisRateLimitingConfiguration
{
public static IServiceCollection AddDistributedRateLimiting(
this IServiceCollection services,
IConfiguration configuration)
{
var redisConnection = configuration.GetConnectionString("Redis");
services.AddSingleton<IConnectionMultiplexer>(
ConnectionMultiplexer.Connect(redisConnection!));
services.AddRateLimiter(options =>
{
options.AddPolicy("distributed", context =>
{
var redis = context.RequestServices
.GetRequiredService<IConnectionMultiplexer>();
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier)
?? context.Connection.RemoteIpAddress?.ToString()
?? "anonymous";
return RateLimitPartition.Get(
partitionKey: userId,
factory: key => new RedisRateLimiter(
redis: redis,
partitionKey: key,
options: new RedisRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
});
});
return services;
}
}
/// <summary>
/// Custom rate limiter using Redis for distributed state
/// </summary>
public sealed class RedisRateLimiter : RateLimiter
{
private readonly IConnectionMultiplexer _redis;
private readonly string _partitionKey;
private readonly RedisRateLimiterOptions _options;
public RedisRateLimiter(
IConnectionMultiplexer redis,
string partitionKey,
RedisRateLimiterOptions options)
{
_redis = redis;
_partitionKey = partitionKey;
_options = options;
}
public override TimeSpan? IdleDuration => null;
public override RateLimiterStatistics? GetStatistics() => null;
protected override async ValueTask<RateLimitLease> AcquireAsyncCore(
int permitCount,
CancellationToken cancellationToken)
{
var db = _redis.GetDatabase();
var key = $"rate_limit:{_partitionKey}";
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var windowStart = now - (long)_options.Window.TotalSeconds;
// Remove old entries outside the window
await db.SortedSetRemoveRangeByScoreAsync(key, 0, windowStart);
// Count current requests in window
var currentCount = await db.SortedSetLengthAsync(key);
if (currentCount >= _options.PermitLimit)
{
// Get oldest entry to calculate retry-after
var oldest = await db.SortedSetRangeByRankWithScoresAsync(key, 0, 0);
var retryAfter = oldest.Length > 0
? TimeSpan.FromSeconds(oldest[0].Score + _options.Window.TotalSeconds - now)
: _options.Window;
return new RedisRateLimitLease(false, retryAfter);
}
// Add new request
await db.SortedSetAddAsync(key, Guid.NewGuid().ToString(), now);
await db.KeyExpireAsync(key, _options.Window.Add(TimeSpan.FromMinutes(1)));
return new RedisRateLimitLease(true, null);
}
protected override RateLimitLease AttemptAcquireCore(int permitCount)
{
// Synchronous not supported for Redis
return new RedisRateLimitLease(false, _options.Window);
}
}
public sealed class RedisRateLimitLease : RateLimitLease
{
private readonly TimeSpan? _retryAfter;
public RedisRateLimitLease(bool isAcquired, TimeSpan? retryAfter)
{
IsAcquired = isAcquired;
_retryAfter = retryAfter;
}
public override bool IsAcquired { get; }
public override IEnumerable<string> MetadataNames =>
_retryAfter.HasValue ? new[] { MetadataName.RetryAfter.Name } : Array.Empty<string>();
public override bool TryGetMetadata(string metadataName, out object? metadata)
{
if (metadataName == MetadataName.RetryAfter.Name && _retryAfter.HasValue)
{
metadata = _retryAfter.Value;
return true;
}
metadata = null;
return false;
}
}
public sealed class RedisRateLimiterOptions
{
public int PermitLimit { get; set; } = 100;
public TimeSpan Window { get; set; } = TimeSpan.FromMinutes(1);
}
Algorithm Selection Guide
┌────────────────────────────────────────────────────────────────┐
│ Which Rate Limiting Algorithm? │
├────────────────────────────────────────────────────────────────┤
│ │
│ Need to limit concurrent requests (not rate)? │
│ │ │
│ └── YES ──► Use Concurrency Limiter │
│ (file uploads, heavy operations) │
│ │
│ Need to allow bursts? │
│ │ │
│ └── YES ──► Use Token Bucket │
│ (API calls, user actions) │
│ │
│ Need smooth rate limiting? │
│ │ │
│ └── YES ──► Use Sliding Window │
│ (API rate limits, quota enforcement) │
│ │
│ Simple time-based limit? │
│ │ │
│ └── YES ──► Use Fixed Window │
│ (login attempts, simple limits) │
│ │
└────────────────────────────────────────────────────────────────┘
Critical Rules
- Choose the right algorithm - Token bucket for bursts, sliding window for smooth limits
- Partition by user - Avoid IP-only partitioning (NAT issues)
- Set queue limits - Prevent memory exhaustion
- Include Retry-After header - Help clients back off properly
- Log rejections - Monitor for abuse patterns
- Test under load - Verify limits work as expected
- Use distributed state - Redis for multi-instance deployments
- Exempt health checks - Don't rate limit monitoring endpoints
- Different limits per tier - Premium users get higher limits
- Middleware order matters - Place before auth for anonymous limits
Anti-Patterns to Avoid
// ❌ WRONG: Partition only by IP (NAT can share IPs)
options.AddPolicy("bad", context =>
RateLimitPartition.GetFixedWindowLimiter(
context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
_ => new FixedWindowRateLimiterOptions { /* ... */ }));
// ✅ CORRECT: Prefer user ID, fallback to IP
options.AddPolicy("good", context =>
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
var partitionKey = userId ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous";
return RateLimitPartition.GetFixedWindowLimiter(partitionKey, /* ... */);
});
// ❌ WRONG: No queue limit (memory exhaustion risk)
options.AddFixedWindowLimiter("bad", limiter =>
{
limiter.PermitLimit = 100;
limiter.Window = TimeSpan.FromMinutes(1);
// Missing QueueLimit!
});
// ✅ CORRECT: Always set queue limit
options.AddFixedWindowLimiter("good", limiter =>
{
limiter.PermitLimit = 100;
limiter.Window = TimeSpan.FromMinutes(1);
limiter.QueueLimit = 10; // Bounded queue
});
// ❌ WRONG: Rate limiting health endpoints
app.MapGet("/health", () => Results.Ok())
.RequireRateLimiting("api");
// ✅ CORRECT: Exempt health checks
app.MapGet("/health", () => Results.Ok())
.DisableRateLimiting();
// ❌ WRONG: Same limits for all users
options.AddFixedWindowLimiter("api", limiter =>
{
limiter.PermitLimit = 100; // Same for everyone
});
// ✅ CORRECT: Tier-based limits
options.AddPolicy("api", context =>
{
var tier = context.User.FindFirstValue("tier") ?? "free";
var limit = tier switch { "premium" => 500, "basic" => 100, _ => 20 };
return RateLimitPartition.GetFixedWindowLimiter(/* ... */);
});
Related Skills
07-minimal-api-endpoints- Apply rate limits to endpoints12-dotnet-jwt-authentication- User identification for partitioning24-logging-configuration- Log rate limit events17-dotnet-health-checks- Exempt from rate limiting