GM.RateLimiting
1.0.0
dotnet add package GM.RateLimiting --version 1.0.0
NuGet\Install-Package GM.RateLimiting -Version 1.0.0
<PackageReference Include="GM.RateLimiting" Version="1.0.0" />
<PackageVersion Include="GM.RateLimiting" Version="1.0.0" />
<PackageReference Include="GM.RateLimiting" />
paket add GM.RateLimiting --version 1.0.0
#r "nuget: GM.RateLimiting, 1.0.0"
#:package GM.RateLimiting@1.0.0
#addin nuget:?package=GM.RateLimiting&version=1.0.0
#tool nuget:?package=GM.RateLimiting&version=1.0.0
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
IRateLimiterService—TryAcquireAsync(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 swappableIRateLimitStore— aGM.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 sameIRateLimiterService— 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
INCRalone 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.Cachingis 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.Middlewaresstyle): resolves the key from the request, applies a policy per endpoint, and returns429+Retry-Afterwith a body matching the GMProblemDetailsshape.- 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 | Versions 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. |
-
net10.0
- GM.Caching (>= 1.0.0)
- GM.DistributedLock (>= 1.0.0)
- GM.Exceptions (>= 1.2.1)
- Microsoft.Extensions.Options (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
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 |