I’ll show a minimal working example (drop-in Program.cs), explain placement, per-endpoint policies, custom rejection handling, testing, and how to scale with Redis for multi-node deployments. I’ll also call out common pitfalls and quick references.
Official samples and docs for ASP.NET Core’s built-in Rate Limiter are my primary sources below. Microsoft Learn+1
1. Prerequisites
.NET 8 SDK installed.
A minimal API or Web API project (
dotnet new webapi -n RateLimitDemo).usinglines you’ll need:
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
(Everything lives in the Microsoft.AspNetCore ecosystem — no extra NuGet for basic use.)
2. Minimal working example (full Program.cs)
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
// 1) Register rate limiting service and policies
builder.Services.AddRateLimiter(options =>
{
// set default rejection status code (optional)
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
// global OnRejected handler (sets Retry-After when available)
options.OnRejected = (context, ct) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString();
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
return ValueTask.CompletedTask;
};
// Example policy: fixed window limiter named "FixedApi"
options.AddFixedWindowLimiter("FixedApi", opt =>
{
opt.PermitLimit = 10; // 10 requests
opt.Window = TimeSpan.FromMinutes(1); // per 1 minute
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 0; // don't queue (reject when exhausted)
});
// Example token-bucket partitioned by IP for burst control (GlobalLimiter)
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
var ip = context.Connection.RemoteIpAddress?.ToString() ?? "anon";
return RateLimitPartition.GetTokenBucketLimiter(ip, _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 100,
TokensPerPeriod = 100,
ReplenishmentPeriod = TimeSpan.FromMinutes(1),
AutoReplenishment = true,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0
});
});
});
var app = builder.Build();
// Middleware ordering: authentication -> UseRateLimiter -> authorization
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseRateLimiter(); // run limiter after auth (so policies can use auth info). Important.
app.UseAuthorization();
// Apply per-endpoint policy
app.MapGet("/open", () => "open endpoint (global limiter applies)");
app.MapGet("/limited", () => "this endpoint is fixed-window limited")
.RequireRateLimiting("FixedApi");
app.Run();
AddFixedWindowLimiter creates a named policy that you attach to endpoints with .RequireRateLimiting("FixedApi"). GlobalLimiter runs before endpoint limiters and can apply a global IP-based token bucket. Microsoft Learn
3. Where to put UseRateLimiter() (order)
Put
UseRateLimiter()afterUseAuthentication()if your policies depend on user identity, and beforeUseAuthorization()(sample follows this). The docs recommend this ordering. Microsoft Learn
4. Rejection handling & headers
You can set
RejectionStatusCode(default is503in some samples) and useOnRejectedto addRetry-Afteror custom body. Example in the snippet sets 429 and usesMetadataName.RetryAfterwhen available.Note: The Concurrency limiter can’t calculate
Retry-After. Microsoft Learn
5. Quick policy recipes (APIs you’ll use)
options.AddFixedWindowLimiter("name", opts => { ... })— fixed window (simple count per window).options.AddSlidingWindowLimiter("name", opts => { ... })— smoother rolling window.RateLimitPartition.GetTokenBucketLimiter(key, factory)— token bucket for bursts.options.AddConcurrencyLimiter("name", opts => { ... })— limit concurrent in-flight requests.
(These helpers are part of the built-in Rate Limiter system.) Microsoft Learn+1
6. Per-endpoint vs Global
GlobalLimiter runs first for every request; endpoint-specific policies (RequireRateLimiting) run afterward. Use global to protect general traffic (IP-based); use endpoint policies to enforce per-user or per-API-key quotas. Microsoft Learn
7. Testing (quick curl examples)
Call limited endpoint repeatedly:
# hit fixed endpoint 12 times
for i in $(seq 1 12); do curl -i http://localhost:5000/limited; echo; done8. Scaling to multiple nodes — Redis / distributed counters
App-level in-memory limiters work on a single instance. For multi-node deployments, you need a shared store (Redis is common). Options:
Implement distributed token bucket or sliding window using Redis (sorted sets or Lua scripts). There are community examples and a sample repo showing a Redis backplane for the .NET built-in limiter.
Other options: put global rate limits on CDN/API gateway (Cloudflare, Azure Front Door, AWS API Gateway) and use app policies for per-user quotas — defense in depth. (Many teams combine gateway + app policies.)
9. Production considerations & pitfalls
Partition key matters: partition by user id or API key for per-user quotas; IP partitioning can be spoofed — MS docs warn about IP-spoofing and DoS. Don’t blindly rely on IP for public endpoints. Microsoft Learn
Queue vs reject: queueing lets a few extra requests wait — good for burst smoothing. But high queue limits can lead to resource pressure.
Concurrency limiter: it controls concurrent in-flight requests but cannot compute
Retry-After. Microsoft LearnLayering: push large-scale blocking to edge (WAF/CDN/API gateway) — apps should enforce per-user fairness and fine-grained limits.
Observability: emit metrics (Prometheus), log OnRejected events, track per-policy rejects so you can tune limits.
10. Useful references / further reading
Microsoft rate limiting overview + samples (core patterns,
OnRejected,GlobalLimiter, sample code). Microsoft Learn+1HttpClientside rate limiting examples (client-side TokenBucket handlers). Microsoft LearnRedis + distributed patterns and community repo for a Redis backplane for .NET rate limiting. GitHub+1
Join the conversation! Your thoughts help the community grow.