feat: add global and burst rate limiters

Three layers of rate limiting, all disabled by default, opt-in via config:

1. Per-IP (existing): 30 req/min per IP
2. Global: server-wide limit across all IPs
   - Lock-free atomic counter for minimal overhead
   - Returns 503 when exceeded
   - Prevents pool exhaustion from distributed attacks
3. Burst: per-IP burst + sustained windows
   - Blocks rapid-fire abuse within seconds
   - Returns 429 with X-RateLimit-Reason header
   - Example: 5 req/5s burst, 60 req/min sustained

Config:
[global_rate_limit]
requests = 0  # disabled by default
window = "1m"

[burst_rate_limit]
burst = 0  # disabled by default
burst_window = "5s"
sustained = 0
sustained_window = "1m"

Env overrides: GLOBAL_RATE_LIMIT_REQUESTS, GLOBAL_RATE_LIMIT_WINDOW,
BURST_RATE_LIMIT_BURST, BURST_RATE_LIMIT_BURST_WINDOW,
BURST_RATE_LIMIT_SUSTAINED, BURST_RATE_LIMIT_SUSTAINED_WINDOW

Full test coverage: concurrent lock-free test, window expiry, disabled states,
IP isolation, burst vs sustained distinction.
This commit is contained in:
Franz Kafka 2026-03-21 18:35:31 +00:00
parent 91ab76758c
commit 13040268d6
7 changed files with 657 additions and 18 deletions

View file

@ -66,3 +66,28 @@ requests = 30
window = "1m"
# How often to clean up stale IP entries (env: RATE_LIMIT_CLEANUP_INTERVAL)
cleanup_interval = "5m"
[global_rate_limit]
# Server-wide rate limit across ALL IPs. Prevents pool exhaustion from
# distributed attacks even when per-IP limits are bypassed via VPNs.
# Returns 503 when exceeded. Set to 0 to disable.
# Env: GLOBAL_RATE_LIMIT_REQUESTS
requests = 0
# Env: GLOBAL_RATE_LIMIT_WINDOW
window = "1m"
[burst_rate_limit]
# Per-IP burst + sustained rate limiting. More aggressive than the standard
# per-IP limiter. Blocks rapid-fire abuse even if the per-minute limit isn't hit.
# Returns 429 with X-RateLimit-Reason header. Set burst to 0 to disable.
#
# Example: burst=5, burst_window="5s" means max 5 requests in any 5-second span.
# sustained=60, sustained_window="1m" means max 60 requests per minute.
# Env: BURST_RATE_LIMIT_BURST
burst = 0
# Env: BURST_RATE_LIMIT_BURST_WINDOW
burst_window = "5s"
# Env: BURST_RATE_LIMIT_SUSTAINED
sustained = 0
# Env: BURST_RATE_LIMIT_SUSTAINED_WINDOW
sustained_window = "1m"