skills/api/rate-limiting/SKILL.md
Use when adding rate limiting or throttling to .NET API endpoints.
npx skillsauth add faysilalshareef/dotnet-ai-kit 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.
429 Too Many Requests with a Retry-After header when limits are exceededSystem.Threading.RateLimiting and Microsoft.AspNetCore.RateLimiting packages (.NET 7+)// Program.cs
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
// Global limiter applied to all endpoints
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
context => RateLimitPartition.GetFixedWindowLimiter(
partitionKey: "global",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 1000,
Window = TimeSpan.FromMinutes(1)
}));
});
// Add middleware — place after authentication but before endpoint routing
app.UseRateLimiter();
Allows a fixed number of requests within a non-overlapping time window. Simplest algorithm but can cause burst traffic at window boundaries.
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", limiterOptions =>
{
limiterOptions.PermitLimit = 100;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
limiterOptions.QueueLimit = 10;
});
});
Divides the window into segments for smoother traffic distribution. Avoids the boundary-burst problem of fixed windows.
builder.Services.AddRateLimiter(options =>
{
options.AddSlidingWindowLimiter("sliding", limiterOptions =>
{
limiterOptions.PermitLimit = 100;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.SegmentsPerWindow = 6; // 10-second segments
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
limiterOptions.QueueLimit = 5;
});
});
Tokens replenish at a steady rate, allowing short bursts up to the bucket capacity while enforcing a long-term average rate.
builder.Services.AddRateLimiter(options =>
{
options.AddTokenBucketLimiter("token", limiterOptions =>
{
limiterOptions.TokenLimit = 100;
limiterOptions.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
limiterOptions.TokensPerPeriod = 20;
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
limiterOptions.QueueLimit = 10;
limiterOptions.AutoReplenishment = true;
});
});
Limits the number of concurrent requests rather than requests per time window. Useful for protecting resources with limited parallelism.
builder.Services.AddRateLimiter(options =>
{
options.AddConcurrencyLimiter("concurrency", limiterOptions =>
{
limiterOptions.PermitLimit = 50;
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
limiterOptions.QueueLimit = 25;
});
});
Apply named policies to specific endpoints or route groups.
// Minimal API
app.MapGet("/api/orders", GetOrders)
.RequireRateLimiting("sliding");
app.MapPost("/api/orders", CreateOrder)
.RequireRateLimiting("token");
// Route group
var api = app.MapGroup("/api/reports")
.RequireRateLimiting("fixed");
api.MapGet("/daily", GetDailyReport);
api.MapGet("/monthly", GetMonthlyReport);
// Disable rate limiting on a specific endpoint
app.MapGet("/api/health", GetHealth)
.DisableRateLimiting();
// Controller attribute
[EnableRateLimiting("sliding")]
[ApiController]
[Route("api/[controller]")]
public sealed class OrdersController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetOrders() { /* ... */ }
[DisableRateLimiting]
[HttpGet("count")]
public async Task<IActionResult> GetOrderCount() { /* ... */ }
}
Partition rate limits by user identity, IP address, or API key so each consumer gets an independent quota.
builder.Services.AddRateLimiter(options =>
{
// Per authenticated user
options.AddPolicy("per-user", context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User?.Identity?.Name ?? "anonymous",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 60,
Window = TimeSpan.FromMinutes(1)
}));
// Per IP address
options.AddPolicy("per-ip", context =>
RateLimitPartition.GetSlidingWindowLimiter(
partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 200,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 4
}));
// Per API key with tiered limits
options.AddPolicy("per-api-key", context =>
{
var apiKey = context.Request.Headers["X-Api-Key"].ToString();
var tier = GetTierForApiKey(apiKey); // Your lookup logic
return tier switch
{
"premium" => RateLimitPartition.GetTokenBucketLimiter(apiKey,
_ => new TokenBucketRateLimiterOptions
{
TokenLimit = 500,
ReplenishmentPeriod = TimeSpan.FromSeconds(10),
TokensPerPeriod = 100,
AutoReplenishment = true
}),
_ => RateLimitPartition.GetTokenBucketLimiter(apiKey,
_ => new TokenBucketRateLimiterOptions
{
TokenLimit = 50,
ReplenishmentPeriod = TimeSpan.FromSeconds(10),
TokensPerPeriod = 10,
AutoReplenishment = true
})
};
});
});
Customize the rejection response to include a Retry-After header and a structured error body.
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.OnRejected = async (context, cancellationToken) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString();
}
context.HttpContext.Response.ContentType = "application/problem+json";
await context.HttpContext.Response.WriteAsJsonAsync(new
{
type = "https://tools.ietf.org/html/rfc6585#section-4",
title = "Too Many Requests",
status = 429,
detail = "Rate limit exceeded. Please retry after the duration indicated in the Retry-After header."
}, cancellationToken);
};
});
| Scenario | Algorithm | Why | |----------|-----------|-----| | Simple request cap per minute | Fixed Window | Easy to understand and configure | | Smooth traffic with no boundary spikes | Sliding Window | Distributes permits across segments | | Allow short bursts with steady average | Token Bucket | Permits accumulate during quiet periods | | Protect CPU/memory-bound operations | Concurrency Limiter | Caps parallel execution, not rate | | Multi-tenant with per-user fairness | Custom Partitioner + any algorithm | Each partition tracks independently | | Login / auth endpoints (brute force) | Fixed Window, low limit | Simple hard cap per IP | | File upload endpoints | Concurrency Limiter | Prevent memory exhaustion from parallel uploads | | Public search API | Sliding Window | Smooth throttling for variable traffic |
| Anti-Pattern | Problem | Fix |
|-------------|---------|-----|
| No Retry-After header on 429 | Clients retry immediately, worsening load | Use OnRejected to set the header from lease metadata |
| Rate limiting after expensive middleware | Work is already done before rejection | Place UseRateLimiter() early in the pipeline |
| Same limit for all endpoints | Health checks and admin routes get throttled | Use per-endpoint policies and DisableRateLimiting() |
| Global-only limits, no per-client partitioning | One abusive client exhausts the quota for everyone | Use AddPolicy with a partition key (user, IP, API key) |
| Unlimited queue depth | Memory grows under sustained overload | Set QueueLimit to a reasonable bound |
| Hardcoded limits with no configuration | Cannot adjust without redeployment | Bind limits from IConfiguration / appsettings.json |
| Rate limiting in every microservice independently | Inconsistent limits, hard to reason about | Centralize at the API gateway when possible |
Microsoft.AspNetCore.RateLimiting package in .csproj filesAddRateLimiter in Program.cs or startup configurationUseRateLimiter() in the middleware pipelineRequireRateLimiting or EnableRateLimiting on endpointsAspNetCoreRateLimit (migration candidate)Microsoft.AspNetCore.RateLimiting (included in the framework shared package)AddRateLimiter() with at least one named policyapp.UseRateLimiter() after authentication, before endpoint mappingRequireRateLimiting("policy-name")OnRejected to return Retry-After and a Problem Details bodydotnet-httpie, bombardier) to verify limitsdata-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.