Programování

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.

12. 10. 2026 · 4 min čtení

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 jinak 503, 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ěm rate-limit-redis pro více instancí).
  • Laravel: vestavěný RateLimiter a middleware throttle: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-After je 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í.