-
Notifications
You must be signed in to change notification settings - Fork 0
The Store
One bucket per (scope, client identity) pair, each holding a sliding-window counter. The store creates them on demand and evicts idle ones.
| Default | |
|---|---|
DefaultCleanupInterval |
5 minutes — how often eviction runs |
DefaultMaxIdle |
10 minutes — how long a bucket may be idle before removal |
DefaultMaxBuckets |
100,000 — the hard cap on distinct buckets |
The store is keyed partly by client identity, which is derived from the request — so without a cap its size is bounded by the number of distinct clients an attacker can present, not by anything the application controls.
Idle eviction alone does not bound it. Eviction runs every five minutes and removes buckets idle for ten; a client rotating source addresses creates them faster than that, and each one survives ten minutes. The steady-state size is (creation rate × 10 minutes) — entirely the attacker's choice.
100,000 is high enough that a real deployment never reaches it and low enough to bound the memory: a bucket is a handful of integers, so the cap costs a few megabytes rather than a few gigabytes.
ratelimit.WithMaxBuckets(50_000) // 0 → the default; negative → no capDisabling the cap is only defensible when the key function has a provably bounded range — a fixed set of API keys, say — and even then the proof tends to expire.
New buckets are not created past it. The practical effect is that a client arriving after the cap is reached is not individually tracked.
If you are hitting the cap in production, the cause is almost always one of:
-
TrustedProxiesis misconfigured in the other direction — trusting an untrusted hop, so a client-suppliedX-Forwarded-Forbecomes the key. See Behind a Proxy. - The key function has unbounded range — keyed by something client-supplied and unhashed. See Client Identity.
- Genuine scale, in which case raise it deliberately and watch the memory.
The store's cleanup goroutine is started in OnInitialize and stopped in
OnStop, so it does not outlive the application.
Buckets are per process and not shared between replicas. Two replicas mean each client gets two budgets — the limit is per instance.
If you need a global limit across replicas, that requires a shared store (Redis and similar), which this extension does not implement. The usual workarounds:
- Divide the intended limit by the replica count. Crude, and wrong during a rolling deploy when the count is temporarily higher.
- Rate limit at the ingress, where there is one of it.
- Accept per-instance limits, which for abuse prevention is usually fine — the attacker's cost still rises linearly.
Each bucket counts requests across a moving window rather than resetting at a fixed boundary.
A fixed window lets a client send its whole budget in the last second of one window and its whole budget again in the first second of the next — twice the intended rate across that boundary, which is exactly the moment a burst matters.
X-RateLimit-Limit: 600 the applicable limit
X-RateLimit-Remaining: 583 what is left in the current window
X-RateLimit-Reset: 1757000000 Unix seconds when the window rolls
Retry-After: 34 on a 429 only
Set on every response, not only refusals — a client that can read its remaining quota can slow down; one that cannot discovers the limit by exceeding it.
For a browser client these must be exposed through CORS, which
rextension-cors does by
default. If you override WithExposedHeaders, include them explicitly — that
option replaces the defaults.
rextension-ratelimit — global, per-router and per-endpoint rate limiting for Rex · MIT · © 2026 Kryovyx
Ecosystem