GM.RateLimiting.Redis
1.0.0
dotnet add package GM.RateLimiting.Redis --version 1.0.0
NuGet\Install-Package GM.RateLimiting.Redis -Version 1.0.0
<PackageReference Include="GM.RateLimiting.Redis" Version="1.0.0" />
<PackageVersion Include="GM.RateLimiting.Redis" Version="1.0.0" />
<PackageReference Include="GM.RateLimiting.Redis" />
paket add GM.RateLimiting.Redis --version 1.0.0
#r "nuget: GM.RateLimiting.Redis, 1.0.0"
#:package GM.RateLimiting.Redis@1.0.0
#addin nuget:?package=GM.RateLimiting.Redis&version=1.0.0
#tool nuget:?package=GM.RateLimiting.Redis&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.RateLimiting (>= 1.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
- StackExchange.Redis (>= 2.13.17)
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 | 92 | 8/5/2026 |