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.

A rate limiter is only as fair as its ability to distinguish one client from another. Dotnet-RateLimiter uses a layered identity resolution strategy implemented in RedisRateLimitingMiddleware to answer the question “who is making this request?” before constructing the Redis key. Authenticated users are tracked by their unique identity claim, anonymous visitors are tracked by their IP address, and every counter is scoped to the specific endpoint path being accessed — so limits are always granular, never blunt.

Authenticated Users

When a request arrives, the middleware first inspects the ASP.NET Core HttpContext to determine whether the caller has been authenticated:
if (context.User.Identity?.IsAuthenticated == true)
{
    var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
                 ?? context.User.Identity.Name
                 ?? "authenticated_unknown";

    identityKey = userId;
    userType = "user";
}
The resolution follows a priority chain:
PrioritySourceDescription
1stClaimTypes.NameIdentifierThe standard claim for a unique, stable user ID (e.g., a GUID or database primary key). This is the preferred identifier.
2ndcontext.User.Identity.NameThe display name claim. Used as a fallback when NameIdentifier is not present in the token.
3rd"authenticated_unknown"A hard-coded sentinel value used when the principal is authenticated but carries neither of the above claims. All such requests share one counter — a safe degradation that prevents unlimited access.
With a resolved userId, the userType is set to "user" and the rate limit key prefix becomes:
user:{userId}:{path}

Anonymous Guests

If the request is not authenticated, the middleware falls back to IP-based identification:
else
{
    var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
    identityKey = ip;
    userType = "guest";
}
context.Connection.RemoteIpAddress is the IP address of the TCP connection as seen by the server. If — in a highly unusual scenario — the IP cannot be resolved, the string "unknown" is used as a sentinel, causing all unidentifiable anonymous requests to share a single counter. With a resolved IP, the userType is set to "guest" and the rate limit key prefix becomes:
guest:{ip}:{path}

Path-Based Scoping

Regardless of whether the caller is authenticated or anonymous, the resolved identity is always combined with the request path:
var limitKey = $"{userType}:{identityKey}:{context.Request.Path}";
context.Request.Path is the raw path component of the URL (e.g., /api/data, /api/export). This means each unique (identity, path) pair gets its own independent counter. Why this matters: Without path scoping, a single rate limit counter would apply to all of a user’s requests across every endpoint. If a user hammers a slow, expensive endpoint like /api/export and exhausts their quota, they would be blocked from fast, lightweight endpoints like /api/status as well — even though those requests impose no meaningful load. Path scoping ensures each endpoint has its own budget per identity.

Redis Key Format

The limitKey assembled by the middleware is passed directly to RedisRateLimiter.IsAllowedAsync, which prepends the global rate_limit: namespace prefix before writing to Redis:
key = (RedisKey)$"rate_limit:{key}"
The complete Redis key format is therefore:
rate_limit:{userType}:{identityKey}:{path}

Concrete Examples

ScenarioRedis Key
Authenticated user user-123 on /api/datarate_limit:user:user-123:/api/data
Authenticated user user-123 on /api/exportrate_limit:user:user-123:/api/export
Guest at 192.168.1.42 on /api/datarate_limit:guest:192.168.1.42:/api/data
Authenticated but no claimsrate_limit:user:authenticated_unknown:/api/data
Unresolvable anonymous IPrate_limit:guest:unknown:/api/data
Each key holds a single integer counter managed by the Lua script. Keys expire automatically after the configured windowSeconds, resetting the counter for the next window.

Identity Resolution in Practice

Below is the complete identity resolution block from RedisRateLimitingMiddleware.cs, showing the full flow from claim inspection to key assembly:
string identityKey;
string userType;

if (context.User.Identity?.IsAuthenticated == true)
{
    var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
                 ?? context.User.Identity.Name
                 ?? "authenticated_unknown";

    identityKey = userId;
    userType = "user";
}
else
{
    var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
    identityKey = ip;
    userType = "guest";
}

var limitKey = $"{userType}:{identityKey}:{context.Request.Path}";
After limitKey is built, it is forwarded to the rate limiter along with the attribute’s configuration values:
var isAllowed = await _rateLimiter.IsAllowedAsync(
    limitKey,
    rateLimitAttr.MaxRequests,
    TimeSpan.FromSeconds(rateLimitAttr.WindowSeconds)
);

Authenticated vs Guest Key Examples

Scenario: A logged-in user whose JWT contains a NameIdentifier claim of user-123 calls GET /api/data. The endpoint is decorated with [RedisRateLimit(maxRequests: 10, windowSeconds: 60)].Resolution steps:
  1. context.User.Identity.IsAuthenticatedtrue
  2. ClaimTypes.NameIdentifier"user-123"
  3. userType"user"
  4. limitKey"user:user-123:/api/data"
  5. Redis key → "rate_limit:user:user-123:/api/data"
On the 11th request within 60 seconds:
  • Redis key rate_limit:user:user-123:/api/data holds counter 11
  • Lua script returns 0 (blocked)
  • Response: 429 Too Many Requests
  • Body: "Dear user, you've reached your limit. Take a breath!"
  • Retry-After: 60
rate_limit:user:user-123:/api/data  →  counter: 11  TTL: 43s
rate_limit:user:user-123:/api/export →  counter: 2   TTL: 51s  ← unaffected

Build docs developers (and LLMs) love