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.

In a modern production environment, a single API rarely runs on a single machine. Horizontal scaling — spinning up multiple instances of your service behind a load balancer — is the standard approach to handling traffic. This architecture, however, exposes a fundamental flaw in traditional rate limiting: in-memory state is local state. Dotnet-RateLimiter solves this by moving the source of truth to Redis, giving every instance of your application a single, consistent counter to reason about.

The Problem with In-Memory Limiters

Standard .NET rate limiting middleware (and most off-the-shelf throttling libraries) stores counters in the process’s own memory. Each instance of your API keeps its own private tally of how many times a given client has made a request. This works perfectly on a single node — but falls apart the moment you scale out.

Each Server Has Its Own State

When you run two (or ten) instances of your API, there is no synchronisation between their in-memory stores. Instance A knows only about requests it has personally handled. Instance B knows only about requests it has personally handled. The load balancer distributes traffic between them, so a single client’s requests are split across machines — and each machine thinks the client is well within its limit.

The Race Condition: Split-Brain Under Load

Consider a limit of 10 requests per 60 seconds. A client rapidly fires requests that are distributed across two API instances behind a load balancer:
  • Instance A has handled 9 requests for this client → its local counter reads 9.
  • Instance B has handled 9 requests for this client → its local counter also reads 9.
  • The next request arrives and lands on Instance A → counter is 9 < 10, allowed
  • A near-simultaneous request also arrives on Instance B → counter is also 9 < 10, allowed
Both instances individually believed they were within the limit. The client has now made 11 requests — 10% over the intended ceiling — and neither instance had any way to know.In high-traffic scenarios with many instances, the over-admission multiplies proportionally: with 5 instances each capped at 10 in-memory, a determined client can push through up to 50 requests against a policy that should allow 10.

A Concrete Two-Instance Scenario

Imagine the following deployment:
Client ──► Load Balancer
               ├── API Instance A  (in-memory counter: 9)
               └── API Instance B  (in-memory counter: 9)
The client sends two requests within the same millisecond. The load balancer routes one to A and one to B. Both instances read their local counter (9), compare it against the limit (10), decide independently that the request is allowed, and increment to 10. The client passed the gate twice simultaneously. There is no coordination, no locking, no awareness of the other instance’s counter. This is not a theoretical edge case. Under real load, requests arrive in tight bursts, and race windows measured in microseconds are hit constantly at scale.

Redis as the Source of Truth

Dotnet-RateLimiter eliminates the split-brain problem by moving counter state entirely out of the application process and into Redis — a single, shared, in-memory data store that all API instances connect to. Instead of each server maintaining its own counter, every instance delegates the “should this request be allowed?” question to Redis. The counter lives in one place, and every instance reads from and writes to that same value.
Client ──► Load Balancer
               ├── API Instance A  ─┐
               └── API Instance B  ─┴──► Redis (counter: 9)
When Instance A and Instance B both receive a request at the same millisecond, they both call into the same Redis key. Redis processes those calls sequentially — not concurrently — so the counter increments from 9 to 10, then from 10 to 11. The second request sees a counter of 11, which exceeds the limit of 10, and is correctly blocked.
Benefits of the Redis-backed approach:
  • Cluster-wide consistency — all API instances share a single counter; there is no divergence between nodes.
  • Horizontal scale-safe — adding more API instances does not weaken the rate limit enforcement.
  • TTL-managed windows — Redis natively handles key expiry, so rate limit windows reset automatically without any background cleanup jobs in your application.
  • Atomic enforcement — Dotnet-RateLimiter uses a Lua script to perform the increment and comparison as a single indivisible operation, closing the remaining TOCTOU window. See Lua Atomicity for details.

Key Scoping

Sharing a counter across instances is only useful if the counter is uniquely scoped to the right combination of who is making the request and what they are accessing. Dotnet-RateLimiter namespaces every Redis key using the following format:
rate_limit:{userType}:{identityKey}:{path}
SegmentSourceExample
rate_limit:Static prefixrate_limit:
{userType}user for authenticated, guest for anonymoususer
{identityKey}Claim / IP addressuser-123
{path}context.Request.Path/api/data
Full example key: rate_limit:user:user-123:/api/data This three-part scoping provides two important guarantees:
  1. Per-user isolation — User A’s counter never affects User B’s counter. Hitting your limit does not penalise anyone else.
  2. Per-endpoint isolation — A user who exhausts the limit on a heavy, expensive endpoint (e.g., /api/export) is not blocked from lighter endpoints (e.g., /api/status). Each path maintains its own counter.
The limitKey is assembled in the middleware before the Redis call:
var limitKey = $"{userType}:{identityKey}:{context.Request.Path}";
RedisRateLimiter.IsAllowedAsync then prepends the rate_limit: prefix when constructing the actual Redis key:
key = (RedisKey)$"rate_limit:{key}"
The result is a fully-qualified, collision-resistant key that scopes every counter to exactly one identity on exactly one endpoint — across every instance in your cluster.

Build docs developers (and LLMs) love