GM.RateLimiting 1.0.0

dotnet add package GM.RateLimiting --version 1.0.0
                    
NuGet\Install-Package GM.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="GM.RateLimiting" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GM.RateLimiting" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="GM.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 GM.RateLimiting --version 1.0.0
                    
#r "nuget: GM.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 GM.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=GM.RateLimiting&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=GM.RateLimiting&version=1.0.0
                    
Install as a Cake Tool

GM.RateLimiting

Distributed rate limiting for the GM.* .NET ecosystem. Enforce request limits across every instance of a scaled-out API or gateway — not per-process like ASP.NET Core's built-in limiter — with a per-policy choice of fixed window, sliding window, or token bucket.

  • Provider-agnostic IRateLimiterServiceTryAcquireAsync(key, policy), callable identically from ASP.NET Core middleware and a YARP/Gateway pipeline.
  • Counters live in GM.Caching; the check-and-increment is made atomic by a swappable IRateLimitStore — a GM.DistributedLock-guarded store by default, or a lock-free Redis Lua store (GM.RateLimiting.Redis) for high throughput.
  • Composable keys: per-user / per-API-key / per-IP / per-endpoint, and combinations.
  • 429-shaped result: limit, remaining, Retry-After, resets-at.

Status: core library. The ASP.NET Core middleware (GM.RateLimiting.Http) and the Gateway/YARP helper are layered on top of this same IRateLimiterService — see Roadmap.

Install

dotnet add package GM.RateLimiting

Register

builder.Services.AddGMCaching();          // or AddGMRedisCaching(...)         — the counter store
builder.Services.AddGMDistributedLock();  // or AddGMRedisDistributedLock(...) — cross-instance atomicity

builder.Services.AddGMRateLimiting(o =>
{
    o.FailOpen = true;   // allow if the store is unavailable (availability > strictness)
    o.Policies["api"]       = new RateLimitPolicy { Algorithm = RateLimitAlgorithm.SlidingWindow, PermitLimit = 100, Window = TimeSpan.FromMinutes(1) };
    o.Policies["burst"]     = new RateLimitPolicy { Algorithm = RateLimitAlgorithm.TokenBucket,  PermitLimit = 20,  Window = TimeSpan.FromSeconds(1), BurstCapacity = 40 };
    o.Policies["login-ip"]  = new RateLimitPolicy { Algorithm = RateLimitAlgorithm.FixedWindow,  PermitLimit = 5,   Window = TimeSpan.FromMinutes(15) };
});

Or bind policies from configuration:

builder.Services.AddGMRateLimiting(builder.Configuration.GetSection("RateLimiting"));
{
  "RateLimiting": {
    "FailOpen": true,
    "Policies": {
      "api":   { "Algorithm": "SlidingWindow", "PermitLimit": 100, "Window": "00:01:00" },
      "burst": { "Algorithm": "TokenBucket",   "PermitLimit": 20,  "Window": "00:00:01", "BurstCapacity": 40 }
    }
  }
}

Use

public sealed class OrdersController(IRateLimiterService limiter)
{
    public async Task<IResult> Create(HttpContext ctx, OrderDto dto)
    {
        // Limit per (user + endpoint): each user gets their own budget on this route.
        var key = RateLimitKey.Compose(
            RateLimitKey.ForUser(ctx.User.FindFirst("sub")!.Value),
            RateLimitKey.ForEndpoint(ctx.Request.Method, ctx.Request.Path));

        var result = await limiter.TryAcquireAsync(key, "api");
        if (!result.IsAllowed)
            return Results.Json(new { error = "rate_limited" }, statusCode: 429);   // + Retry-After: result.RetryAfter

        return await CreateOrderAsync(dto);
    }
}

RateLimitResult carries everything for the response headers: Limit, Remaining, RetryAfter, ResetsAt. For call sites that prefer to throw, RateLimitExceededException (a GM.Exceptions.CustomException) wraps the rejected result.

Algorithms

Algorithm Shape Good for
FixedWindow N per aligned window, reset on the boundary cheapest; simple quotas where a 2× boundary burst is fine
SlidingWindow weights the previous window as it rolls out steady traffic without the boundary burst (default)
TokenBucket refills at a steady rate up to a burst capacity bursty clients with a capped sustained rate

Windows are epoch-aligned, so independent instances agree on the current window with no coordination. Add a new strategy by implementing IRateLimitAlgorithm and registering it by Kind.

Keys

RateLimitKey builds and composes the dimension a limit counts against:

RateLimitKey.ForUser("u123")
RateLimitKey.ForApiKey(rawKey)                 // stored as a short hash, never the raw secret
RateLimitKey.ForIp("203.0.113.7")
RateLimitKey.ForEndpoint("POST", "/orders")
RateLimitKey.Compose(ForUser("u123"), ForEndpoint("POST", "/orders"))  // per-user-per-endpoint

Design notes & flagged trade-offs

Atomicity: distributed lock vs. native atomic (Redis). GM.Caching exposes no atomic increment, and its in-memory single-flight is per-process only — so the correct cross-instance counter needs serialization. Two stores, same IRateLimitStore contract, swap by DI registration:

Store Package How it's atomic Cost
LockGuardedRateLimitStore (default) GM.RateLimiting GM.DistributedLock around a cache read-modify-write; runs the C# algorithm under the lock a lock acquire + get + set per request; correct on any cache backend
RedisRateLimitStore GM.RateLimiting.Redis one atomic Lua script per algorithm, server-side no lock, one round-trip; the high-throughput choice
builder.Services.AddGMRateLimiting(/* policies */);
builder.Services.AddGMRedisRateLimitStore();   // replaces the default with the atomic Lua store
                                               // (needs an IConnectionMultiplexer, e.g. via AddGMRedisCaching)

A plain INCR alone can't do the token bucket (a read-modify-write of a fractional balance), which is why the atomic path is a Lua script — it covers all three algorithms with identical semantics. GM.Caching is deliberately left untouched.

Fail-open vs. fail-closed. If the store can't complete (backend down, lock not acquired within LockWait), FailOpen (default true) allows the request so the limiter never becomes an availability risk. Set it false to reject instead when strict enforcement matters more.

Runtime policy overrides (not built — flagged). Letting GM.Secrets or GM.FeatureManagement override limits at runtime (raise a customer's quota, flip a policy without a deploy) is a natural fit: RateLimitingOptions.Policies is already the single source of policy, so a dynamic IPolicyProvider could layer over it. Deferred pending a decision on the source of truth.

Roadmap

  • GM.RateLimiting.Http — ASP.NET Core middleware (GM.API.Middlewares style): resolves the key from the request, applies a policy per endpoint, and returns 429 + Retry-After with a body matching the GM ProblemDetails shape.
  • Gateway/YARP — the same IRateLimiterService, called in the YARP pipeline for centralized enforcement at the edge.

Done: GM.RateLimiting.Redis — the lock-free atomic Lua store.

License

MIT — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET 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 (3)

Showing the top 3 NuGet packages that depend on GM.RateLimiting:

Package Downloads
GM.RateLimiting.Http

ASP.NET Core middleware for GM.RateLimiting (GM.API.Middlewares style). Partitions each request by user / API key / IP / endpoint (composable), applies a named policy per endpoint via [RateLimit], and on limit-exceeded returns 429 Too Many Requests with a Retry-After header, X-RateLimit-* headers, and a ProblemDetails body consistent with the GM conventions. Register with AddGMRateLimitingHttp() + UseGMRateLimiting().

GM.RateLimiting.Redis

Redis-backed, lock-free store for GM.RateLimiting. RedisRateLimitStore runs each algorithm (fixed window, sliding window, token bucket) as a single atomic Lua script server-side — no distributed lock, one round-trip per check — so limits are enforced across every instance at high throughput. Register with AddGMRedisRateLimitStore() to replace the default lock-guarded store.

GM.Gateway

A YARP-based API gateway for the GM.* ecosystem with distributed rate limiting enforced at the edge. AddGMGateway() wires a YARP reverse proxy from configuration and GM.RateLimiting together; each route opts into a named policy via route metadata (RateLimitPolicy / RateLimitPartition), so limits are enforced centrally at the gateway — across every instance — before a request is proxied, returning 429 + Retry-After for rejects. Pair with GM.RateLimiting.Redis for the lock-free atomic store.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 111 8/5/2026