Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nimanikoo/Dotnet-RateLimiter/llms.txt

Use this file to discover all available pages before exploring further.

The Redis Fixed Window strategy is the flagship distributed rate-limiting mechanism in Dotnet-RateLimiter. Unlike the in-memory ASP.NET Core policies, this strategy stores all counters in Redis and enforces limits atomically across every instance of your application. Whether you run two pods or two hundred, every request is counted against a single shared window — making it the correct choice for any multi-instance or cloud-native deployment.

How It Works

Each incoming request increments a Redis counter scoped to the caller’s identity, user type, and request path. The counter lives for exactly one fixed time window. When the counter exceeds the configured maximum, the request is rejected immediately — no queuing. The critical guarantee comes from the atomic Lua script: the INCR and EXPIRE operations are sent to Redis as a single indivisible script, so there is no race condition between two application instances reading and writing the counter concurrently.
Window:  |-------- N seconds --------|-------- N seconds --------|
Counter: 1  2  3  4  5 → REJECT      1  2  3  ...

The [RedisRateLimit] Attribute

Apply rate limiting declaratively to any controller action with the RedisRateLimitAttribute. Its exact signature is:
public RedisRateLimitAttribute(int maxRequests, int windowSeconds)
ParameterTypeDescription
maxRequestsintMaximum number of requests allowed within the window
windowSecondsintDuration of the fixed window in seconds

Controller Example

The following endpoint allows 2 requests per 10 seconds per caller, taken directly from TestController.cs:
[HttpGet("limited")]
[RedisRateLimit(maxRequests: 2, windowSeconds: 10)]
public IActionResult GetLimited() => Ok("This is a limited endpoint!");
Endpoints without the attribute are completely unaffected by the Redis middleware:
[HttpGet("unlimited")]
public IActionResult GetUnlimited() => Ok("This is unlimited! Sky is the limit.");

The Lua Script

The atomic enforcement lives in RedisRateLimiter.cs. The Lua script is prepared once and evaluated against Redis on every request:
local current = redis.call('INCR', @key)
if tonumber(current) == 1 then
    redis.call('EXPIRE', @key, @window)
end
if tonumber(current) > tonumber(@maxRequests) then
    return 0
else
    return 1
end
Step-by-step:
  1. INCR atomically increments the counter and returns the new value.
  2. If this is the first increment (current == 1), EXPIRE sets the key’s TTL to windowSeconds. This starts the clock only once per window.
  3. If the counter exceeds maxRequests, the script returns 0 (deny); otherwise it returns 1 (allow).
Because Lua scripts execute atomically inside a single Redis server thread, there is no possibility of two concurrent requests both seeing current == 1 and both skipping the expiry step.

IsAllowedAsync Method

The service method called by the middleware:
public virtual async Task<bool> IsAllowedAsync(string key, int maxRequests, TimeSpan window)
ParameterDescription
keyComposite identity key (built by the middleware — see below)
maxRequestsMaximum allowed requests, sourced from the attribute
windowTimeSpan built from windowSeconds on the attribute
Returns true when the request is permitted, false when the limit is exceeded.

Redis Key Naming

The middleware constructs the identity key before calling IsAllowedAsync:
var limitKey = $"{userType}:{identityKey}:{context.Request.Path}";
RedisRateLimiter then prefixes it with rate_limit: when writing to Redis:
key = (RedisKey)$"rate_limit:{key}"
The final key stored in Redis follows this pattern:
rate_limit:{userType}:{identityKey}:{path}
SegmentAuthenticated callerAnonymous caller
userTypeuserguest
identityKeyUser ID or Identity.NameRemote IP address
path/Test/limited/Test/limited
Example keys:
rate_limit:user:abc-123-user-id:/Test/limited
rate_limit:guest:203.0.113.42:/Test/limited
This design means limits are enforced per-user per-endpoint, so a single user hitting one endpoint does not consume quota from another endpoint.

429 Response Behavior

When IsAllowedAsync returns false, the middleware short-circuits the pipeline and writes:
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.Response.Headers.RetryAfter = rateLimitAttr.WindowSeconds.ToString();
  • Status code: 429 Too Many Requests
  • Retry-After header: set to the full windowSeconds value so clients know when to retry
  • Response body: a human-readable message that differs by caller type:
    • Authenticated: "Dear user, you've reached your limit. Take a breath!"
    • Anonymous: "Guest limit reached. Please sign up for more quota!"

Middleware Registration

The middleware is registered in Program.cs after routing but before endpoint execution:
app.UseRouting();
app.UseMiddleware<RedisRateLimitingMiddleware>();
app.UseEndpoints(endpoints => endpoints.MapControllers());

When to Use

ScenarioRecommendation
Multi-instance / Kubernetes deployment✅ Use Redis Fixed Window
Need per-user, per-endpoint granularity✅ Use Redis Fixed Window
Single-node development environmentConsider In-Memory policies
Need smooth traffic distribution across timeConsider Sliding Window
Fixed Window boundary bursts. Because the window resets at a hard clock boundary, a caller can legally fire maxRequests requests at the very end of one window and another maxRequests requests at the very start of the next window. This means up to 2 × maxRequests requests can arrive within a span of milliseconds straddling the boundary. If this burst pattern is a concern for your downstream services, consider the Sliding Window strategy instead.

Build docs developers (and LLMs) love