Rate limiting¶
Reference for the rate_limit bootstrap section, the cost table, and the response headers. For the setup walkthrough,
see Configure rate limiting; for the model, see the
how-to's overview.
Configuration¶
rate_limit lives only in the trusted bootstrap file, never in the sourced tree.
rate_limit:
enabled: true
backend: memory # memory | redis
redis:
addr: redis:6379
password: env:REDIS_PASSWORD
db: 0
trusted_proxies: [ "10.0.0.0/8" ]
bypass_cidrs: [ "10.1.2.0/24" ]
ip: { capacity: 60, refill_per_sec: 1 }
principal: { capacity: 120, refill_per_sec: 2 }
costs:
issue: { auth_error: 12 }
task_trigger: { success: 10 }
| Field | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | false |
Turns limiting on. When false, config vet warns the endpoint is unprotected. |
backend |
string | memory |
memory (per instance) or redis (shared across replicas). |
redis.addr |
string | - | Required when backend: redis. |
redis.password |
secret.Ref |
- | Optional Redis password. |
redis.db |
int | 0 |
Redis database number. |
trusted_proxies |
list of CIDRs | none | Proxy CIDRs whose X-Forwarded-For is trusted for client-IP derivation. |
bypass_cidrs |
list of CIDRs | none | Client CIDRs that skip both layers entirely. |
ip.capacity |
int | - | Burst ceiling for the per-IP bucket. |
ip.refill_per_sec |
number | - | Tokens added per second to the per-IP bucket. |
principal.capacity |
int | - | Burst ceiling for the per-principal bucket. |
principal.refill_per_sec |
number | - | Tokens added per second to the per-principal bucket. |
costs |
map | defaults | Per-(category, class) overrides; unspecified cells keep their defaults. |
capacity and refill_per_sec must be positive; CIDRs must parse; costs must be non-negative.
config vet reports any violation as CFG-RATELIMIT.
Cost table¶
Cost is a function of (category, outcome class). The category comes from the route (edge) or the service call site;
the outcome class comes from the HTTP status (2xx success, 401 auth error,
403 denied, other 4xx client error, 5xx server error) or the internal outcome on the token path.
| category | success | denied | auth_error | client_error | server_error |
|---|---|---|---|---|---|
issue |
1 | 4 | 10 | 3 | 1 |
revoke |
1 | 4 | 10 | 3 | 1 |
explain |
1 | 2 | 8 | 2 | 1 |
providers |
2 | 2 | 8 | 2 | 1 |
resolve |
2 | 2 | 8 | 2 | 1 |
audit |
3 | 3 | 8 | 2 | 1 |
task_trigger |
8 | 8 | 8 | 4 | 1 |
session_login |
2 | 2 | 10 | 3 | 1 |
webhook |
1 | 1 | 10 | 2 | 1 |
| default | 2 | 3 | 8 | 2 | 1 |
Clean requests are cheap; auth failures and heavy operations are expensive; 5xx is cheap so clients are not punished
for server-side outages. Override any cell under costs; unspecified cells keep the default.
Backends and failure behavior¶
memorykeeps buckets in a sharded in-memory map with a background sweeper that evicts idle keys. Fine for a single instance; state does not survive a restart and is not shared.rediskeeps buckets in Redis via atomic Lua scripts, so all replicas share one view. If Redis is unavailable, the limiter falls back to a local in-memory bucket, logs it, and counts the fallback. It never fails open; during a Redis outage the effective limit degrades to per-instance.
The limiter is a stable component: it survives config reloads without resetting its state.
Response headers¶
- On rejection:
429 Too Many RequestswithRetry-After(seconds until the bucket is positive again) and a small JSON body carrying the correlation id. - On every response:
RateLimit-Limit,RateLimit-Remaining, andRateLimit-Reset(draft IETF fields) for the tightest bucket that applied. /healthzand/icanhaztalmiare always exempt, as arebypass_cidrspeers.