429 Too Many Requests: rate limiting и обход
Код 429 Too Many Requests — сигнал от сервера: «ты слишком быстро, сбавь темп». Это стандартный ответ rate limiter'а, защищающего API документацию и сайты от злоупотреблений. Разберём, как правильно внедрить 429 на сервере, как клиенту корректно обработать Retry-After, и что делать, если ваш сервис получает 429 от внешнего API.
Что означает 429
Согласно RFC 6585 §4, 429 указывает, что пользователь отправил слишком много запросов за единицу времени. Это отличие от 503 (сервис недоступен) и 403 (запрещено политикой). 429 — временное состояние, клиент может повторить запрос после Retry-After.
Обязательный заголовок: Retry-After: 60 (секунды) или дата. Также рекомендуются неформальные заголовки:
X-RateLimit-Limit— максимум запросов в окнеX-RateLimit-Remaining— осталось запросовX-RateLimit-Reset— Unix timestamp сброса окна
Стратегии rate limiting
Token Bucket
Ведро с токенами пополняется с фиксированной скоростью. Каждый запрос забирает токен. Пусто — 429.
Leaky Bucket
Очередь фиксированного размера, обрабатывается с постоянной скоростью. Переполнена — 429.
Fixed Window
Счётчик на минуту/час. Простой, но даёт всплески на границах окна.
Sliding Window
Гибрид: учитывает запросы в предыдущем окне пропорционально. Самый равномерный подход.
Настройка в nginx
# Zone: 10MB хватит на ~160k уникальных IP
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;
limit_req_status 429;
proxy_pass http://backend;
}
location /api/auth/login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}Параметры:
rate=60r/m— 1 запрос в секунду в среднемburst=20— допускает всплеск до 20, остальные отбрасываютсяnodelay— не задерживает, сразу отдаёт 429 при превышении
Rate limiting на уровне приложения (Redis sliding window)
<?php
function checkRateLimit(Redis $redis, string $key, int $max, int $window): array {
$now = microtime(true);
$pipe = $redis->multi();
$pipe->zRemRangeByScore($key, 0, $now - $window);
$pipe->zAdd($key, $now, "$now:" . bin2hex(random_bytes(4)));
$pipe->zCard($key);
$pipe->expire($key, $window * 2);
$results = $pipe->exec();
$count = $results[2];
return [
'allowed' => $count <= $max,
'limit' => $max,
'remaining' => max(0, $max - $count),
'reset' => time() + $window,
];
}
// Использование
$rl = checkRateLimit($redis, "rl:user:{$userId}", 60, 60);
header("X-RateLimit-Limit: {$rl['limit']}");
header("X-RateLimit-Remaining: {$rl['remaining']}");
header("X-RateLimit-Reset: {$rl['reset']}");
if (!$rl['allowed']) {
http_response_code(429);
header('Retry-After: 60');
header('Content-Type: application/json');
echo json_encode(['error' => 'Too many requests']);
exit;
}Обработка 429 на клиенте (exponential backoff)
async function fetchWithRetry(url, options = {}, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429) return res;
const retryAfter = res.headers.get('Retry-After');
const waitMs = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.min(1000 * Math.pow(2, attempt), 60000);
console.warn(`429 received, retrying in ${waitMs}ms`);
await new Promise(r => setTimeout(r, waitMs));
}
throw new Error('Rate limit exceeded after retries');
}Никогда не ретрайте агрессивно без backoff — это усугубит rate limit и может триггерить бан на уровне WAF.
Идентификация клиента: IP, User, API Key
Правильный tracker зависит от контекста:
- API с ключами — лимит по API Key
- Логин-эндпоинты — по IP (препятствует brute force)
- Пользовательские действия — по user_id (чтобы NAT не блокировал много пользователей одного IP)
// Иерархия: user ID > API key > IP
function getRateLimitKey(Request $req): string {
if ($req->user?->id) return "user:{$req->user->id}";
if ($req->apiKey) return "apikey:{$req->apiKey}";
return "ip:{$req->ip}";
}429 от внешнего API: что делать
- Читайте
Retry-After— это не рекомендация, а требование. - Используйте circuit breaker — временно отключайте запросы к деградирующему API.
- Кэшируйте ответы Redis'ом с разумным TTL.
- Пакуйте запросы в массовую проверку URL, если API поддерживает.
- Попросите повышения лимита — обычно платные тарифы дают больше.
Для мониторинга доступности вашего API используйте Enterno.io Monitors с expected_code=200 — увидите 429-регрессии моментально.
429 vs 503 vs 403
| Код | Когда использовать |
|---|---|
| 429 | Клиент превысил rate limit |
| 503 | Сервер перегружен (общая проблема, не клиентская) |
| 403 | Политика доступа (клиент заблокирован полностью) |
Часто задаваемые вопросы (FAQ)
В: Влияет ли 429 на SEO?
О: Для Googlebot — да, если частый. Googlebot уменьшает crawl rate. Поддерживайте Retry-After и не блокируйте official Googlebot IP.
В: Какой rate limit для API адекватен?
О: Зависит. Типичные значения: 60 req/min для общих API, 5 req/min для login, 10 req/sec для реалтайм-эндпоинтов.
В: Можно ли обойти 429?
О: Честный путь: запросить повышение лимита, оптимизировать количество запросов, кэшировать. Обход через ротацию IP = нарушение ToS и риск бана.
Заключение
429 защищает ваш сервис и партнёров. Настройте корректный rate limit (sliding window + Redis), всегда возвращайте Retry-After и X-RateLimit-* заголовки, а на клиенте используйте exponential backoff. Для мониторинга API-эндпоинтов — Enterno.io.
Проверьте ваш сайт прямо сейчас
Проверить →