Rate limiting API: algoritmy, Nginx, Redis a hlavičky 429
Jak omezit počet požadavků na API: fixed window, sliding window a token bucket, nastavení limit_req v Nginxu, implementace v Redisu a správné odpovědi 429.
Bez omezení počtu požadavků stačí jeden chybný skript klienta nebo bot zkoušející hesla a API leží. Rate limiting chrání server, férově rozděluje kapacitu mezi uživatele a brzdí útoky hrubou silou. Ukážeme hlavní algoritmy a dvě praktické implementace – na úrovni Nginxu a v aplikaci s Redisem.
Podle čeho limitovat
- IP adresa – jednoduché, funguje i pro nepřihlášené. Pozor na NAT (celá firma za jednou IP) a na IPv6, kde má útočník k dispozici obrovské rozsahy – limitujte raději celý prefix /64.
- Uživatel nebo API klíč – férovější, umožňuje různé limity podle tarifu.
- Endpoint – přihlášení nebo reset hesla potřebují mnohem přísnější limit než čtení katalogu.
V praxi se kombinuje: hrubý limit na IP v proxy a jemné limity podle uživatele v aplikaci.
Algoritmy
| Algoritmus | Princip | Vlastnosti |
|---|---|---|
| Fixed window | počítadlo na minutu, na začátku další minuty se vynuluje | nejjednodušší; na hranici oken projde až dvojnásobek |
| Sliding window log | ukládá časy všech požadavků | přesný, paměťově náročný |
| Sliding window counter | vážený průměr aktuálního a předchozího okna | dobrý kompromis |
| Token bucket | kyblík se plní stálou rychlostí, požadavek odebere žeton | povoluje krátké nárazy (burst) |
| Leaky bucket | požadavky odtékají stálou rychlostí | vyhlazuje provoz; používá ho Nginx |
Nginx: limit_req
Rate limiting na úrovni reverzní proxy odfiltruje provoz dřív, než se dostane k aplikaci.
V bloku http:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_status 429;
V bloku server:
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location = /api/login {
limit_req zone=login burst=3;
proxy_pass http://127.0.0.1:3000;
}
Co to znamená:
$binary_remote_addr– klíčem je IP klienta (binárně, šetří paměť). Zóna o velikosti 10 MB pojme zhruba 160 000 IPv4 adres.rate=10r/s– průměrně 10 požadavků za sekundu.burst=20– až 20 požadavků navíc se může nahromadit ve frontě.nodelay– požadavky v burstu se zpracují okamžitě, nečekají na „odkapání“.limit_req_status 429– výchozí odpověď je jinak503, což je pro klienta zavádějící.
Pokud je Nginx sám za další proxy nebo CDN, musíte nejdřív správně nastavit skutečnou IP klienta (modul realip), jinak omezíte celou CDN najednou.
Ověření a reload:
sudo nginx -t && sudo systemctl reload nginx
Aplikace: fixed window v Redisu
Pro limity podle uživatele potřebujete sdílené počítadlo pro všechny instance aplikace. Redis na to je ideální díky atomickému INCR:
import time
import redis
r = redis.Redis()
def allow_request(user_id: str, limit: int = 100, window: int = 60) -> tuple[bool, int, int]:
now = int(time.time())
window_start = now - (now % window)
key = f"rl:{user_id}:{window_start}"
pipe = r.pipeline()
pipe.incr(key)
pipe.expire(key, window)
count, _ = pipe.execute()
remaining = max(0, limit - count)
reset = window_start + window
return count <= limit, remaining, reset
Pipeline s MULTI/EXEC (výchozí transaction=True v redis-py) zajistí, že INCR a EXPIRE proběhnou společně a klíč nezůstane bez expirace.
Token bucket v Redisu
Pro plynulejší chování s povolenými nárazy se token bucket implementuje Lua skriptem, protože čtení i zápis stavu musí proběhnout atomicky:
-- KEYS[1] = klíč, ARGV: kapacita, tokeny za sekundu, čas v ms
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(data[1]) or capacity
local ts = tonumber(data[2]) or now
tokens = math.min(capacity, tokens + (now - ts) / 1000 * rate)
local allowed = 0
if tokens >= 1 then
tokens = tokens - 1
allowed = 1
end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(capacity / rate * 1000))
return allowed
Skript zaregistrujete přes r.register_script(lua) a voláte s klíčem a argumenty.
Hotové knihovny
Nemusíte všechno psát ručně:
- Express:
express-rate-limit(s úložištěmrate-limit-redispro více instancí). - Laravel: vestavěný
RateLimitera middlewarethrottle:60,1. - Django REST Framework: třídy
UserRateThrottle,AnonRateThrottle. - FastAPI / Starlette:
slowapi.
Správná odpověď klientovi
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
Content-Type: application/json
{"error": "Příliš mnoho požadavků, zkuste to za 30 sekund."}
Retry-Afterje standardní hlavička HTTP a říká klientovi, kdy to zkusit znovu.- Hlavičky
RateLimit-*vycházejí z návrhu standardu IETF; starší API používajíX-RateLimit-*. Hlavní je posílat je konzistentně a zdokumentovat.
Klienti by na 429 měli reagovat exponenciálním backoffem s náhodným rozptylem, ne okamžitým opakováním.
Kontrola, že limit funguje
for i in $(seq 1 30); do
curl -s -o /dev/null -w "%{http_code}\n" https://api.example.cz/api/ping
done | sort | uniq -c
Měli byste vidět část odpovědí 200 a zbytek 429. Zkontrolujte i to, že limit jednoho uživatele neovlivní jiného a že po uplynutí okna se požadavky zase propouštějí.