Ограничение частоты (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 с джиттером.
Проверьте себя
1. Какой HTTP-код возвращается при превышении лимита частоты?
A403 Forbidden
B429 Too Many Requests
C503 Service Unavailable
D409 Conflict
2. В чём слабость алгоритма fixed window?
Aон требует слишком много памяти
Bна стыке двух окон возможен всплеск до 2N запросов
Cон не поддерживает лимит по IP
Dон не сбрасывает счётчик
3. Что должен сделать клиент, получив 429 с заголовком Retry-After: 30?
Aнемедленно повторить запрос
Bподождать ~30 секунд перед повтором
Cсменить IP и повторить
Dпрекратить любые запросы навсегда
4. Зачем добавлять джиттер (случайную добавку) при экспоненциальном backoff?
Aчтобы запросы были быстрее
Bчтобы множество клиентов не повторяли запросы синхронно (thundering herd)
Cчтобы обойти лимит
Dэто требование стандарта HTTP