API Rate Limiting: token bucket, 429, Retry-After
API Rate Limiting: token bucket, 429, Retry-After
Rate limiting — это ограничение частоты запросов к API документацию на одного клиента. Цели: предотвратить брутфорс auth-эндпоинтов, защитить от DDoS, сохранить инфраструктуру от злоупотребления тяжёлыми запросами, справедливо распределить ресурсы между тарифы. В статье разберём алгоритмы (token bucket, sliding window), HTTP 429 + Retry-After и реализации на Redis, nginx, Express.
Зачем rate limiting
- Защита от credential stuffing (автоперебор паролей)
- Защита от scraping и data exfiltration
- Контроль затрат: Google Maps API, OpenAI — платные, и без лимита счёт улетит в космос
- Бизнес-дифференциация: free=100/день, pro=10000/день
- Fair-use: один клиент не может заблокировать сервис для всех
Алгоритмы
Fixed Window
Счётчик сбрасывается каждую минуту/час. Плюсы: просто. Минусы: «граничный burst» — 100 запросов в 23:59 и ещё 100 в 00:00 = 200 за 2 секунды.
Sliding Window
Учитывает скользящее окно последних N секунд. Точнее, но дороже по памяти. Реализуется через Redis sorted set с timestamp'ами:
ZADD ratelimit:user:123 {now} {now}
ZREMRANGEBYSCORE ratelimit:user:123 0 {now - 60}
ZCARD ratelimit:user:123 ← текущее количество за последние 60 сек
EXPIRE ratelimit:user:123 120
Token Bucket
Бакет вмещает N токенов, пополняется со скоростью M/сек. Каждый запрос забирает 1 токен. Если пусто — 429. Хорошо работает для burst-трафика (кэш-прогрев, массовую проверку URL-запрос).
Leaky Bucket
Обратный к token bucket: запросы заливаются в очередь фиксированной скоростью. Smooth traffic, но не даёт burst'ов.
HTTP 429 и Retry-After
При превышении лимита возвращайте:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705324800
Content-Type: application/json
{"error":"rate_limit_exceeded","message":"Try again in 30 seconds"}
Retry-After — в секундах или как HTTP-дата. Клиенты (curl, браузеры) умеют уважать этот заголовок с backoff.
Реализация на Redis (PHP)
function checkRateLimit(Redis $r, string $key, int $max, int $window): bool {
$now = microtime(true);
$r->zAdd("rl:{$key}", $now, "{$now}");
$r->zRemRangeByScore("rl:{$key}", 0, $now - $window);
$count = $r->zCard("rl:{$key}");
$r->expire("rl:{$key}", $window * 2);
return $count <= $max;
}
if (!checkRateLimit($redis, "user:{$userId}", 60, 60)) {
header('HTTP/1.1 429 Too Many Requests');
header('Retry-After: 60');
exit(json_encode(['error' => 'rate_limit_exceeded']));
}
nginx
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
location /api/auth/login {
limit_req zone=login burst=3 nodelay;
proxy_pass http://backend;
}
}
Express / NestJS
// Express
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
app.use('/api/', rateLimit({
store: new RedisStore({ client: redisClient }),
windowMs: 60_000,
max: 60,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: (req) => req.user?.id || req.ip,
}));
// NestJS
ThrottlerModule.forRoot({
throttlers: [{ ttl: 60_000, limit: 60 }],
storage: new RedisThrottlerStorage(redis),
})
Ключ rate limit: IP vs user vs API key
- IP — ловит анонимные атаки, но NAT даёт много false-positive в офисах
- User ID — точнее, если клиент залогинен
- API key — для публичного API, позволяет дифференцированные тарифы
Лучшая практика — комбинация: user_id || api_key || ip.
Auth-эндпоинты: особые правила
Login/register/password-reset: максимум 5 попыток в 15 минут per IP + per email. Используйте задержку bcrypt (cost 12+), добавьте CAPTCHA после 3 неудач.
Мониторинг
Следите за метриками: процент 429, топ-10 IP по 429, latency при rate limit. Рекомендую настроить мониторинг на rising 429 rate — это сигнал о DDoS или сломанном клиенте.
FAQ
Чем отличается 429 от 503? 429 — клиент превысил свой лимит, 503 — сервер перегружен независимо от клиента.
Нужен ли Retry-After? Очень желательно — без него клиенты делают busy-loop, ухудшая ситуацию.
Как защититься от распределённого DDoS? Rate limit на приложении не хватит — нужен Cloudflare/AWS Shield или WAF.
Можно ли обойти rate limit? Через ротацию IP (прокси), ротацию API-ключей, бесплатные триалы. Защита — фингерпринтинг + behavioural analysis.
Вывод
Минимум: sliding-window на Redis, 429+Retry-After, отдельный жёсткий лимит на auth-эндпоинты. Для серьёзных угроз — WAF. Мониторьте API через enterno monitors. Связанное: CORS, HTTP Security Headers.
Проверьте ваш сайт прямо сейчас
Проверить →