Pinqponq.RateLimiting 1.0.0

Prefix Reserved
dotnet add package Pinqponq.RateLimiting --version 1.0.0
                    
NuGet\Install-Package Pinqponq.RateLimiting -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Pinqponq.RateLimiting" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Pinqponq.RateLimiting" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Pinqponq.RateLimiting" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Pinqponq.RateLimiting --version 1.0.0
                    
#r "nuget: Pinqponq.RateLimiting, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Pinqponq.RateLimiting@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Pinqponq.RateLimiting&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Pinqponq.RateLimiting&version=1.0.0
                    
Install as a Cake Tool

Pinqponq.RateLimiting

ASP.NET Core rate limiting set up in one call. You name the policies and their numbers. The package registers the limiters, partitions requests by client address, user or your own key, and answers a rejected request with 429 Too Many Requests, a Retry-After header and the standard error body.

Install

dotnet add package Pinqponq.RateLimiting

Requirements

  • .NET 8.0, 9.0, or 10.0
  • ASP.NET Core (the package uses the built-in rate limiting middleware)
  • Pinqponq.ErrorHandling (installed with this package) for the error body

Quick start

builder.Services.AddPinqponqRateLimiting(options =>
{
    options.Policies.Add("sign-in", new RateLimitPolicyOptions
    {
        PermitLimit = 30,
        Window = TimeSpan.FromMinutes(2),
        PartitionBy = RateLimitPartitionSource.UserOrClientAddress,
    });
    options.GlobalLimits.Add(new RateLimitPolicyOptions
    {
        PermitLimit = 1200,
        Window = TimeSpan.FromMinutes(1),
    });
});

var app = builder.Build();
app.UseForwardedHeaders();   // first, so the client address is the real one behind a proxy
app.UseRouting();
app.UseRateLimiter();        // after UseRouting, so endpoint policies are found
app.MapControllers();

Apply a named policy with the standard ASP.NET Core attribute or endpoint method:

[EnableRateLimiting("sign-in")]
public sealed class SignInController : ControllerBase { /* ... */ }

app.MapPost("/sign-in", SignIn).RequireRateLimiting("sign-in");

The numbers above are only an example. The package has no default limit or window: each product chooses its own.

Configuration

AddPinqponqRateLimiting(Action<RateLimitingOptions>) sets the options in code. AddPinqponqRateLimiting(IConfiguration section, Action<RateLimitingOptions>? configure) binds them from a configuration section (for example RateLimiting) and then runs configure, for settings that configuration cannot hold, such as PartitionKeyResolver.

{
  "RateLimiting": {
    "Policies": {
      "sign-in": { "PermitLimit": 30, "Window": "00:02:00", "PartitionBy": "UserOrClientAddress" }
    },
    "GlobalLimits": [
      { "PermitLimit": 1200, "Window": "00:01:00" }
    ]
  }
}

Define GlobalLimits in one settings file only: .NET merges a list from two settings files element by element.

RateLimitingOptions Default Notes
Policies empty Named policies for [EnableRateLimiting(name)] and RequireRateLimiting(name).
GlobalLimits empty Limits applied to every request. With several, a request must be allowed by each of them.
UserIdClaimType ClaimTypes.NameIdentifier Claim that identifies the user. The JWT bearer handler maps sub to this claim while its MapInboundClaims is on, which is the default.
RateLimitPolicyOptions Default Notes
PermitLimit none Requests allowed per partition in one window. Required, greater than 0.
Window none Length of the fixed window. Required, greater than 0.
PartitionBy ClientAddress What requests are counted by (see below).
PartitionKeyResolver none Func<HttpContext, string?> for Custom. Required for Custom, and only allowed there.
IsPartitionKeySecret false Keep the Custom value only as its SHA-256 digest. Set it for API keys and other secrets. Only allowed for Custom.

Every setting is validated at startup. The application does not start when a limit or window is not greater than 0, a policy name is empty, a Custom policy has no resolver, another policy sets a resolver or IsPartitionKeySecret, or UserIdClaimType is empty.

A request over a limit is rejected at once. It is never queued. The global limits are checked before the named policy, so a request counts against the global limits also when its named policy rejects it.

Partitions

Each distinct value gets its own budget. Values from different sources never share a budget, even when their text is the same.

PartitionBy Counted by When the value is missing
ClientAddress HttpContext.Connection.RemoteIpAddress One shared partition for all requests without an address
User The UserIdClaimType claim The request is not limited by this policy. Use it only on endpoints that require authentication.
UserOrClientAddress The user, otherwise the client address Client address
Custom The value PartitionKeyResolver returns Client address

A request without a client address is counted in one shared partition. The package logs a warning the first time this happens, because it usually means UseForwardedHeaders is missing or comes after UseRateLimiter.

A limit per API key and per address

A key sent in a request header identifies a caller, but a caller can send a different key with every request. Chain a limit per key with a looser limit per client address, so changing the key does not buy a new budget. keyHeaderName is the name of your product's key header:

builder.Services.AddPinqponqRateLimiting(options =>
{
    options.GlobalLimits.Add(new RateLimitPolicyOptions
    {
        PermitLimit = 250,
        Window = TimeSpan.FromMinutes(1),
        PartitionBy = RateLimitPartitionSource.Custom,
        PartitionKeyResolver = context => context.Request.Headers[keyHeaderName].FirstOrDefault(),
        IsPartitionKeySecret = true,
    });
    options.GlobalLimits.Add(new RateLimitPolicyOptions
    {
        PermitLimit = 1500,
        Window = TimeSpan.FromMinutes(1),
        PartitionBy = RateLimitPartitionSource.ClientAddress,
    });
});

Rejected requests

A rejected request gets:

  • Status 429.
  • Retry-After: the seconds until the window ends, rounded up and at least 1. The header is left out only when the limiter gives no time.
  • The standard error body of Pinqponq.ErrorHandling with the response code too_many_requests (CommonErrorCodes.TooManyRequests):
{ "status": false, "statusCode": 429, "message": "Too many requests. Try again later.", "responseCode": "too_many_requests", "traceId": "..." }

With AddPinqponqErrorLocalization() the message comes from the error message catalog in the language of the request. The failure is also described on IRequestErrorFeature with the event name request_rate_limited (RateLimitingEvents.RequestRateLimited), so Pinqponq.Observability's request log carries it.

To return another body, register your own IRateLimitRejectionWriter. The status code and the Retry-After header are already set when it runs:

builder.Services.AddSingleton<IRateLimitRejectionWriter, PlainRejectionWriter>();

internal sealed class PlainRejectionWriter : IRateLimitRejectionWriter
{
    public Task Write(HttpContext context, int? retryAfterSeconds, CancellationToken cancellationToken) =>
        context.Response.WriteAsJsonAsync(new { error = "rate_limited", retryAfter = retryAfterSeconds }, cancellationToken);
}

What stays in the product

Which endpoints get which policy, the limits and windows, the claim types and header names that identify a tenant, and any product-specific body.

Repository

github.com/pinqponq/pinqnuqets

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 53 10/21/2026