cli/dist/skills/24-dotnet-rate-limiting/SKILL.md
Implements ASP.NET Core rate limiting middleware for API protection. Covers fixed window, sliding window, token bucket, and concurrency limiters with custom policies.
npx skillsauth add ronnythedev/dotnet-clean-architecture-skills dotnet-rate-limitingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Built-in rate limiting middleware (ASP.NET Core 7+) protects APIs from abuse:
| 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 |
/API/RateLimiting/
├── RateLimitingConfiguration.cs
├── Policies/
│ ├── UserRateLimitPolicy.cs
│ └── IpRateLimitPolicy.cs
└── Middleware/
└── RateLimitExceededHandler.cs
// 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();
// 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);
}
}
// 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;
}
{
"RateLimiting": {
"AnonymousRequestsPerMinute": 30,
"AuthenticatedBurstLimit": 100,
"AuthenticatedTokensPerSecond": 10,
"PremiumBurstLimit": 500,
"PremiumTokensPerSecond": 50,
"ApiRequestsPerMinute": 60,
"MaxConcurrentUploads": 5,
"UploadQueueSize": 10
}
}
// 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)
})
};
});
});
// 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();
}
}
// 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();
}
}
// 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);
}
// 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);
}
┌────────────────────────────────────────────────────────────────┐
│ 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) │
│ │
└────────────────────────────────────────────────────────────────┘
// ❌ 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(/* ... */);
});
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 limitingtools
Implements the Options pattern for strongly-typed configuration in .NET. Covers IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T> with validation and reload support.
tools
SQL Server database design best practices, naming conventions, indexing strategies, and performance optimization for .NET applications using Microsoft.Data.SqlClient and EF Core.
data-ai
PostgreSQL database design best practices, naming conventions, indexing strategies, and performance optimization for .NET applications using Npgsql and EF Core.
development
Implements ASP.NET Core rate limiting middleware for API protection. Covers fixed window, sliding window, token bucket, and concurrency limiters with custom policies.