ASP.NET Core Rate Limiting Middleware Deep Dive: IRateLimiterPolicy and Endpoint Configuration
ASP.NET Core rate limiting middleware evaluates every request against PartitionedRateLimiter instances configured via RateLimiterOptions, applying named policies or custom IRateLimiterPolicy implementations bound to endpoints through EnableRateLimitingAttribute metadata.
The rate limiting system in the dotnet/aspnetcore repository provides a flexible, DI-driven architecture for throttling HTTP requests. Built around the RateLimiterOptions configuration object and the extensible IRateLimiterPolicy interface, this middleware integrates directly with endpoint routing to enforce limits globally or on specific routes.
Core Architecture and Service Registration
The middleware relies on services registered through AddRateLimiter(), which is defined in RateLimiterServiceCollectionExtensions.cs. This extension method configures RateLimiterOptions, registers metrics services, and prepares the policy resolution pipeline.
RateLimiterOptions and Policy Storage
RateLimiterOptions (implemented in RateLimiterOptions.cs) serves as the central configuration hub. It stores three critical elements:
GlobalLimiter– APartitionedRateLimiter<HttpContext>applied to every request before endpoint-specific evaluation.PolicyMap– A dictionary of activated named policies (DefaultRateLimiterPolicyinstances).UnactivatedPolicyMap– A dictionary of policy types that implementIRateLimiterPolicy<TPartitionKey>, which the middleware instantiates via DI on first use.
You register policies using either AddPolicy<TPartitionKey>(name, partitioner) for direct partitioner functions or AddPolicy<TPartitionKey, TPolicy>(name) for custom policy classes.
The IRateLimiterPolicy Interface
Custom policies implement IRateLimiterPolicy<TPartitionKey> from IRateLimiterPolicy.cs. This interface requires two members:
public interface IRateLimiterPolicy<TPartitionKey>
{
Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; }
RateLimitPartition<TPartitionKey> GetPartition(HttpContext httpContext);
}
The GetPartition method returns a RateLimitPartition that determines which rate limiter instance applies to the current request based on the partition key (e.g., client IP, user ID). The optional OnRejected callback executes when a request exceeds the limit, allowing custom response headers or logging.
Configuring Rate Limiting Policies
Named Policies with Direct Partitioners
For simple scenarios, define a partitioner directly in Program.cs:
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy<string>("fixed", ctx =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown",
replenishPeriod: TimeSpan.FromSeconds(10),
permitLimit: 5,
queueLimit: 0));
});
This registers a named policy "fixed" that applies a fixed-window limiter keyed by remote IP address.
Custom Policy Classes
For complex logic or reusable components, implement IRateLimiterPolicy:
public sealed class SlidingWindowPolicy : IRateLimiterPolicy<string>
{
public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected => null;
public RateLimitPartition<string> GetPartition(HttpContext httpContext)
{
var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
return RateLimitPartition.GetSlidingWindowLimiter(
partitionKey: ip,
window: TimeSpan.FromSeconds(30),
permitLimit: 20,
queueLimit: 5);
}
}
Register the policy type:
builder.Services.AddRateLimiter(opts =>
{
opts.AddPolicy<string, SlidingWindowPolicy>("sliding");
});
Global Rate Limiters
To apply limits to all requests regardless of endpoint metadata, configure the GlobalLimiter property:
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: "global",
replenishPeriod: TimeSpan.FromSeconds(1),
permitLimit: 100,
queueLimit: 0));
});
Endpoint Metadata and Middleware Execution
Applying Policies to Endpoints
The middleware discovers policies through endpoint metadata. The EnableRateLimitingAttribute (defined in EnableRateLimitingAttribute.cs) links a policy name or instance to an endpoint. You apply this metadata using the RequireRateLimiting extension method from RateLimiterEndpointConventionBuilderExtensions.cs:
app.UseRateLimiter();
app.MapGet("/api/data", () => "data")
.RequireRateLimiting("fixed");
app.MapGet("/api/premium", () => "premium")
.RequireRateLimiting(new SlidingWindowPolicy());
The middleware also supports DisableRateLimitingAttribute to explicitly bypass limits for specific endpoints.
Middleware Request Flow
The RateLimitingMiddleware (in RateLimitingMiddleware.cs) processes requests through the following stages:
- Endpoint Resolution – Retrieves the endpoint via
context.GetEndpoint(). - Disable Check – If
DisableRateLimitingAttributeis present, the request proceeds immediately. - Policy Resolution – If
EnableRateLimitingAttributeis found, the middleware resolves the correspondingPartitionedRateLimiterfrom the policy map or falls back to the global limiter. - Lease Acquisition – Calls
CombinedAcquirefor instant evaluation; if insufficient permits exist,CombinedWaitAsynchandles queued or delayed acquisition. - Success Path – Records metrics and invokes the next middleware.
- Rejection Path – Sets the
RejectionStatusCode(default 503), records failure metrics, and invokes theOnRejectedcallback if configured.
Internally, the middleware converts user-defined partitioners into a unified PartitionedRateLimiter<DefaultKeyType> using the ConvertPartitioner helper method in RateLimiterOptions.cs to prevent key collisions between different policy types.
Handling Rejected Requests
You can configure rejection behavior globally or per-policy:
builder.Services.AddRateLimiter(opts =>
{
opts.OnRejected = (ctx, ct) =>
{
ctx.HttpContext.Response.Headers.Add("Retry-After", "60");
return ValueTask.CompletedTask;
};
});
For policy-specific rejection handling, return a delegate from the OnRejected property in your IRateLimiterPolicy implementation.
Summary
- Service Registration – Call
AddRateLimiter()inProgram.csto configureRateLimiterOptionsand register required services. - Policy Types – Use
AddPolicywith a partitioner function for simple cases, or implementIRateLimiterPolicy<TPartitionKey>for complex logic with DI support. - Endpoint Binding – Apply
[EnableRateLimiting("policyName")]or useRequireRateLimiting()to attach rate limits to specific routes. - Middleware Flow – The middleware checks
DisableRateLimitingAttribute, resolves policies from endpoint metadata, and attempts lease acquisition throughRateLimitingMiddleware.cs. - Extensibility – Customize rejection responses via
OnRejectedcallbacks inRateLimiterOptionsor within individual policy implementations.
Frequently Asked Questions
What is the difference between the GlobalLimiter and endpoint-specific policies?
The GlobalLimiter applies to every request that passes through the middleware pipeline, regardless of endpoint metadata. Endpoint-specific policies only activate when an endpoint has EnableRateLimitingAttribute metadata. The middleware evaluates the global limiter first, then the endpoint-specific limiter, effectively creating a two-tier throttling system.
How do I implement a custom IRateLimiterPolicy?
Create a class implementing IRateLimiterPolicy<TPartitionKey> (defined in IRateLimiterPolicy.cs), implement the GetPartition method to return a RateLimitPartition, and optionally provide an OnRejected callback. Register it using options.AddPolicy<TPartitionKey, YourPolicy>("name") in RateLimiterOptions.cs. The middleware instantiates your policy via DI when the first request targeting that policy arrives.
Can I disable rate limiting for specific endpoints?
Yes. Apply the [DisableRateLimiting] attribute to controllers, actions, or use the DisableRateLimiting() extension method on endpoint conventions. When the middleware in RateLimitingMiddleware.cs detects this metadata, it bypasses all limit checks for that request, including the GlobalLimiter.
What happens when a request exceeds the rate limit?
The middleware sets the response status code to 503 Service Unavailable (configurable via RejectionStatusCode), records the rejection in RateLimiterMetrics, and invokes the OnRejected callback. If the request was queued (waiting for a permit), the middleware cancels the lease attempt and short-circuits the pipeline.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →