Ограничение частоты (rate limiting)
Любой публичный API должен уметь говорить «достаточно» — иначе один клиент уронит сервис для всех.
Rate limiting — ограничение числа запросов от одного клиента за интервал времени, чтобы защитить сервис от перегрузки и злоупотреблений.
Без ограничения частоты API беззащитен: сломанный клиент в цикле, скрипт-скрапер или DDoS способны исчерпать базу данных, очереди и CPU, и страдать будут все пользователи. Rate limiting вводит честные правила: «не больше N запросов за период». Это не только защита, но и инструмент тарификации (free-план — 60 запросов в минуту, платный — 6000) и предсказуемости нагрузки.
У ограничения частоты есть и точечные применения помимо общей защиты. На эндпоинте логина строгий лимит мешает перебору паролей (brute-force): несколько неудачных попыток — и адрес временно блокируется. На дорогих операциях (генерация отчёта, экспорт) лимит не даёт одному клиенту монополизировать ресурсы. А на бесплатных тарифах лимит — это бизнес-граница, мягко подталкивающая активных пользователей к платному плану. Поэтому грамотный API часто имеет не один глобальный лимит, а разные пороги для разных эндпоинтов и ролей.
Алгоритмы ограничения
Fixed window (фиксированное окно)
Время делится на равные окна (например, по минуте). Счётчик считает запросы в текущем окне и сбрасывается в начале следующего. Просто и дёшево, но есть «эффект границы»: клиент может сделать N запросов в конце одной минуты и сразу N в начале следующей — итого 2N за пару секунд.
Окно 12:00:00–12:00:59 → лимит 100 Окно 12:01:00–12:01:59 → лимит 100 Всплеск на стыке: 100 в 12:00:59 + 100 в 12:01:00 = 200 за ~1 сек
Sliding window (скользящее окно)
Окно «скользит» вместе с текущим моментом: учитываются запросы за последние 60 секунд относительно сейчас, а не относительно фиксированной границы. Это сглаживает всплески на стыках ценой более сложного учёта (хранение времён запросов или взвешенная аппроксимация по двум соседним окнам).
Token bucket (корзина токенов)
В «корзину» с фиксированной ёмкостью равномерно капают токены (например, 10 в секунду). Каждый запрос забирает один токен; нет токенов — запрос отклоняется. Корзина допускает короткие всплески (если накопились токены), но держит средний темп. Это самый гибкий и популярный алгоритм.
Ёмкость 10, пополнение 2 токена/сек старт: [●●●●●●●●●●] 10 запросов подряд возможны (burst) далее: ~2 запроса/сек — устойчивый темп
Код 429 и заголовки
Когда лимит исчерпан, API отвечает 429 Too Many Requests. Хороший API не просто отказывает, а сообщает клиенту состояние лимита через заголовки:
curl -i https://api.example.com/v1/search?q=rest
Ответ при исчерпанном лимите:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1700000060
Retry-After: 30
Content-Type: application/json
{
"error": "rate_limit_exceeded",
"message": "Too many requests, retry in 30 seconds"
}
| Заголовок | Смысл |
X-RateLimit-Limit | сколько запросов разрешено в окне |
X-RateLimit-Remaining | сколько ещё осталось в текущем окне |
X-RateLimit-Reset | когда окно сбросится (Unix-время или секунды) |
Retry-After | через сколько секунд можно повторить |
Эти заголовки уместно слать в каждом успешном ответе, а не только при 429 — тогда клиент видит, как тает остаток, и заранее замедляется.
По чему считать лимит: ключ, IP, пользователь
- По API-ключу — точнее всего для авторизованных интеграций: каждый клиент имеет свой тариф.
- По IP — для анонимных запросов, но грубо: за одним NAT (офис, мобильный оператор) сидят тысячи людей с одним IP.
- По пользователю — когда один аккаунт ходит с разных устройств; лимит привязан к
user_idиз токена.
На практике комбинируют: жёсткий лимит по IP против анонимного флуда плюс более щедрый по ключу для платных клиентов. Важно и то, что считать единицей: иногда лимитируют не число запросов, а «стоимость» — тяжёлый поисковый запрос списывает больше «единиц», чем лёгкое чтение по id. Такой взвешенный лимит честнее отражает реальную нагрузку, хотя и сложнее в реализации.
Как клиенту корректно отступать (backoff)
Получив 429, клиент не должен сразу повторять — это усугубляет затор. Правильная стратегия — уважать Retry-After, а если его нет, применять экспоненциальный backoff с джиттером (случайной добавкой), чтобы тысячи клиентов не ломились одновременно:
попытка 1 → ждать ~1с попытка 2 → ждать ~2с попытка 3 → ждать ~4с попытка 4 → ждать ~8с + случайный джиттер 0..1с к каждой паузе
def backoff_delay(attempt, retry_after=None):
import random
if retry_after is not None:
return retry_after # сервер сказал — слушаемся
base = 2 ** attempt # 1, 2, 4, 8, ...
jitter = random.random() # 0..1 сек
return base + jitter
for a in range(4):
print(f"attempt {a}: wait ~{backoff_delay(a):.2f}s")
Вывод:
attempt 0: wait ~1.37s attempt 1: wait ~2.08s attempt 2: wait ~4.55s attempt 3: wait ~8.91s
(Конкретные дробные части случайны — важна растущая база и разброс.)
Как работает под капотом
Счётчики лимитов обычно живут в быстромin-memory хранилище вроде Redis — там атомарные операции INCR с EXPIRE на ключ ratelimit:<user>:<window> позволяют считать запросы без гонок и автоматически чистить старые окна. Token bucket реализуют, храня пару «число токенов + время последнего пополнения» и досчитывая накопившиеся токены в момент запроса (lazy refill), а не по таймеру. Лимитер ставят как можно ближе к краю сети — на API-gateway или балансировщике, чтобы отбракованный запрос не доходил до дорогих сервисов.
Частые ошибки
- Отдавать
429безRetry-After— клиент не знает, когда повторить, и долбит вслепую. - Лимит только по IP — рушит легитимных пользователей за общим NAT.
- Клиент повторяет немедленно и без джиттера — синхронный «громовой топот» (thundering herd).
- Использовать
403или503вместо специализированного429. - Fixed window без понимания эффекта границы — реальный пик вдвое выше заявленного.
- Считать лимит на каждом инстансе отдельно вместо общего хранилища — суммарный лимит размывается.
Итоги
- Rate limiting защищает сервис и лежит в основе тарифов.
- Fixed window прост, но даёт всплеск на стыке; sliding window сглаживает; token bucket допускает burst при среднем темпе.
- Исчерпание лимита →
429 Too Many Requests. - Заголовки
X-RateLimit-Limit/Remaining/ResetиRetry-Afterсообщают клиенту состояние. - Лимит считают по ключу, IP или пользователю — часто в комбинации.
- Клиент обязан отступать: уважать
Retry-Afterили экспоненциальный backoff с джиттером.