A purely distributed limiter pays one network round trip to a shared store on every request — exactly the wrong cost when traffic (and attacks) spike. ThrottleKit’s two-tier engine fronts the distributed store (L2) with a local in-process tier (L1) and lets you choose the consistency/throughput trade-off per limiter, with a provably bounded global overshoot in leased mode.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/AmeyaBorkar/throttlekit/llms.txt
Use this file to discover all available pages before exploring further.
The three modes
twoTier returns a Limiter with three selectable coordination modes:
| Mode | Network cost | Global accuracy | Best for |
|---|---|---|---|
strict | 1 round trip / request | Exact | Hard quotas, billing |
cached-deny | 1 round trip / allowed request | Exact for allows, local for denies | Public APIs under abuse |
leased | ~1 round trip / batch requests | Provably bounded overshoot | High-throughput internal APIs |
strict — exact, one round trip per request
Every check consults L2 directly. There is no local state;strict mode is equivalent to using rateLimit with a distributed store. Use it when global exactness matters more than throughput — billing quotas, hard contractual caps.
cached-deny — protect the protector
Allowed traffic still hits L2 (so allows stay globally exact), but denials are cached locally for theirretryAfterMs. Once a key is over the limit, further requests are rejected from L1 with no round trip, so an abusive client cannot translate a flood of requests into L2 load.
l1.maxKeys to bound memory against a flood of unique (e.g. spoofed-IP) keys.
leased — near-zero network, bounded overshoot
Each node atomically leases a batch ofB credits from L2 in one round trip, then serves up to B requests entirely from L1 — no network at all. When the local budget is exhausted, the node leases another batch.
The overshoot bound
In baseline leased mode (withoutwindowCoupled), credits carry across window boundaries. The tight bound, machine-checked in TLA⁺:
N is the number of nodes. Each node may hold up to Batch − 1 unconsumed local credits from the prior window’s last lease, so across N nodes that is up to N × (Batch − 1) extra admissions on top of a fresh full Limit.
windowCoupled: true collapses the bound to exactly Limit, independent of fleet size. When the L2 window that granted a key’s credits rolls over (now >= lastDecision.resetAt), those credits expire instead of carrying across the boundary. Since cross-window carryover was the only overshoot source:
spec/GaleWindowCoupledLeasing.tla and reproduced by an exhaustive BFS checker that runs in CI.
The lease lifecycle
When a request arrives inleased mode, the check loop:
- Serves locally — if
credits >= cost, decrement and return an allow. Zero network. - Leases on shortage — when
credits < cost, atomically leasemax(batch, cost)from L2; on success, add the grant to local credits and retry. - Coalesces concurrent misses — the in-flight lease promise is stored on the key entry; concurrent misses on the same key
awaitthe same promise instead of each issuing their own lease. This bounds a node to at mostbatchoutstanding at any time — the assumption the overshoot bound rests on. - Surfaces global exhaustion — when L2 is globally out of budget and nothing remains locally, the node returns L2’s denial.
Full example: leased mode with MemoryStore as L2
This example runs standalone and deterministically usingMemoryStore as the L2, as in the two-tier-leased example:
The twoTier API
The algorithm enforced at L2 (and the unit of the leased budget). Any built-in or custom strategy is accepted.
The distributed store (e.g.
RedisStore, PostgresStore, or any Store implementation).The coordination mode. See the mode table above.
Required for
leased mode. Controls the batch size, low-water mark, idle reclamation, and window coupling.batch— credits leased from L2 per round trip. Required unlessadaptiveis set.lowWater— proactively refill when local credits fall to this level (default0; purely on-demand).windowCoupled— expire credits at the L2 window boundary; collapses the overshoot bound to exactlyLimit(defaultfalse).returnIdleAfterMs— return idle credits to L2 after this many ms (optional).adaptive— enable online EOQ lease sizing; the learner targets√(2·orderCost·demand/strandPenalty)per key (experimental).
Local-tier tuning.
maxKeys bounds the in-process maps (deny cache for cached-deny, credit entries for leased) against adversarial key floods.Injected time source. Defaults to
systemClock. Pass a ManualClock in tests.Key namespace — prepended to every key before it reaches L2. Lets one store back many independent limiters.
windowCoupled and fleet-size independence
windowCoupled: true is the keystone of GALE (ThrottleKit’s provable distributed leasing engine). Here is the intuition:
Without window coupling, each node can hold up to Batch − 1 unconsumed credits from the previous window’s last lease. At the window boundary, L2 resets to Limit, but those leftover credits are still valid — so N nodes each holding Batch − 1 leftover credits can admit up to Limit + N × (Batch − 1) requests in the new window. This sum grows with N.
With windowCoupled: true, leftover credits are discarded at the boundary. L2 starts the new window at Limit, and no node carries forward any prior-window credit. Global admissions in the new window therefore cannot exceed Limit — independent of how many nodes are in the fleet.
When to use each mode
strict— hard billing quotas, contractual per-user limits, any case where a single extra request is unacceptable. Pay one round trip per request.cached-deny— public APIs under potential abuse, authentication endpoints, any case where an attacker hammering a blocked key should not cost you Redis load. Pays one round trip per allowed request.leased— high-throughput internal APIs, service-to-service calls, any case where the round-trip cost ofstrictwould dominate latency. Pays approximately one round trip perbatchrequests, with a known overshoot you choose.
leased mode is opt-in, not the default. It trades a small, bounded global overshoot for throughput. That is the right call for high-traffic internal services but wrong for hard billing quotas — always use strict or cached-deny for the latter.