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.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 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
A Concrete Two-Instance Scenario
Imagine the following deployment: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.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:| Segment | Source | Example |
|---|---|---|
rate_limit: | Static prefix | rate_limit: |
{userType} | user for authenticated, guest for anonymous | user |
{identityKey} | Claim / IP address | user-123 |
{path} | context.Request.Path | /api/data |
rate_limit:user:user-123:/api/data
This three-part scoping provides two important guarantees:
- Per-user isolation — User A’s counter never affects User B’s counter. Hitting your limit does not penalise anyone else.
- 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.
limitKey is assembled in the middleware before the Redis call:
RedisRateLimiter.IsAllowedAsync then prepends the rate_limit: prefix when constructing the actual Redis key:
