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 – A PartitionedRateLimiter<HttpContext> applied to every request before endpoint-specific evaluation.
  • PolicyMap – A dictionary of activated named policies (DefaultRateLimiterPolicy instances).
  • UnactivatedPolicyMap – A dictionary of policy types that implement IRateLimiterPolicy<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:

  1. Endpoint Resolution – Retrieves the endpoint via context.GetEndpoint().
  2. Disable Check – If DisableRateLimitingAttribute is present, the request proceeds immediately.
  3. Policy Resolution – If EnableRateLimitingAttribute is found, the middleware resolves the corresponding PartitionedRateLimiter from the policy map or falls back to the global limiter.
  4. Lease Acquisition – Calls CombinedAcquire for instant evaluation; if insufficient permits exist, CombinedWaitAsync handles queued or delayed acquisition.
  5. Success Path – Records metrics and invokes the next middleware.
  6. Rejection Path – Sets the RejectionStatusCode (default 503), records failure metrics, and invokes the OnRejected callback 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() in Program.cs to configure RateLimiterOptions and register required services.
  • Policy Types – Use AddPolicy with a partitioner function for simple cases, or implement IRateLimiterPolicy<TPartitionKey> for complex logic with DI support.
  • Endpoint Binding – Apply [EnableRateLimiting("policyName")] or use RequireRateLimiting() to attach rate limits to specific routes.
  • Middleware Flow – The middleware checks DisableRateLimitingAttribute, resolves policies from endpoint metadata, and attempts lease acquisition through RateLimitingMiddleware.cs.
  • Extensibility – Customize rejection responses via OnRejected callbacks in RateLimiterOptions or 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →