원클릭으로
rate-limiting
Use when adding rate limiting or throttling to .NET API endpoints.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when adding rate limiting or throttling to .NET API endpoints.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use when designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.
Use when writing async code, propagating CancellationTokens, or fixing async/await pitfalls.
Use when applying or enforcing C# coding style — namespaces, sealed classes, var usage, XML docs.
Use when applying modern C# idioms — records, pattern matching, primary constructors, collection expressions.
Use when registering services, choosing lifetimes, or implementing DI patterns like decorator or keyed services.
Use when selecting or implementing design patterns in C# — factory, builder, strategy, decorator, or mediator.
| name | rate-limiting |
| description | Use when adding rate limiting or throttling to .NET API endpoints. |
| metadata | {"category":"api","agent":"api-designer"} |
| when_to_use | When implementing rate limiting policies or throttling for API endpoints |
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 limits