Представь: ты запускаешь API для мобильного приложения. Всё работает, первые пользователи довольны. Потом кто-то пишет скрипт, который долбит твой эндпоинт /search 500 раз в секунду — то ли парсер, то ли баг в клиентском коде, то ли просто злой умысел. База начинает задыхаться, латентность растёт, остальные пользователи видят таймауты. Поздравляю — у тебя нет rate limiting.

Rate limiting — это ограничение количества запросов, которые клиент может сделать к API за определённый промежуток времени. Звучит просто, но дьявол, как обычно, в деталях.

Зачем это вообще нужно

Есть несколько сценариев, где rate limiting спасает:

Защита от DDoS и брутфорса. Без ограничений эндпоинт /login становится приглашением для перебора паролей. Даже медленный брутфорс в 10 запросов в секунду — это 864 000 попыток в сутки.

Честное распределение ресурсов. Один жадный клиент не должен съедать всю пропускную способность. Особенно актуально для публичных API, где сотни пользователей.

Защита от багов в клиентском коде. Разработчик случайно написал бесконечный цикл с запросами — без rate limiting это мгновенно ронает сервер.

Монетизация. Бесплатный тариф — 100 запросов в минуту, платный — 10 000. Классическая модель для SaaS.

Стабильность под нагрузкой. Даже без злого умысла, пиковый трафик (акция, вирусный пост) может положить сервис. Rate limiting работает как предохранитель.

Алгоритмы: как это работает внутри

Не все rate limiters одинаковы. Алгоритм определяет поведение при перегрузке — насколько плавно и предсказуемо.

Fixed Window (фиксированное окно)

Самый простой вариант: считаем запросы в фиксированном временном окне. Например, не более 100 запросов в минуту. Счётчик сбрасывается ровно в начале каждой минуты.

Проблема — граничный эффект. Клиент может сделать 100 запросов в 00:59 и ещё 100 в 01:00 — итого 200 запросов за две секунды реального времени. Для большинства задач это некритично, но знать об этом нужно.

Sliding Window (скользящее окно)

Улучшенная версия: смотрим не на фиксированный интервал, а на последние N секунд относительно текущего момента. Граничный эффект исчезает, но реализация сложнее — нужно хранить временные метки запросов.

В Redis это делается через sorted sets: добавляем timestamp каждого запроса, удаляем старые записи вне окна, считаем оставшиеся.

ZADD requests:user123 <timestamp> <request_id>
ZREMRANGEBYSCORE requests:user123 0 <timestamp - window_size>
ZCARD requests:user123

Если результат меньше лимита — пропускаем запрос. Иначе — возвращаем 429.

Token Bucket (ведро токенов)

Идея: у каждого клиента есть «ведро» с токенами. Каждый запрос забирает один токен. Ведро пополняется с фиксированной скоростью — например, 10 токенов в секунду, максимум 100 токенов.

Главное преимущество — разрешает краткосрочные всплески. Если клиент не делал запросов 10 секунд, у него накопилось 100 токенов — он может сделать burst из 100 запросов моментально. Потом снова ждёт пополнения.

Это хорошо для реальных паттернов использования: пользователь нажимает кнопки активно, потом пауза, снова активно.

Leaky Bucket (дырявое ведро)

Зеркальный подход: запросы попадают в очередь (ведро), из которой они вытекают с постоянной скоростью. Если ведро переполнено — запрос отклоняется.

Обеспечивает максимально ровный поток запросов к бэкенду. Полезно, когда downstream-сервисы плохо переносят всплески нагрузки.

Где ограничивать: клиент, IP, пользователь или ключ?

Гранулярность — важный архитектурный выбор.

По IP. Просто в реализации, но легко обходится через прокси и VPN. Кроме того, за одним IP может сидеть корпоративный офис с сотнями пользователей — слишком жёсткий лимит поломает их работу.

По API-ключу. Стандарт для публичных API. Каждый клиент получает уникальный ключ, лимиты привязаны к нему. Позволяет тонко управлять квотами: разные тарифы, временные исключения, блокировка конкретного злоумышленника без вреда для остальных.

По пользователю (userId). Подходит для авторизованных API. Один пользователь — один лимит, независимо от устройства или IP.

По эндпоинту. Разные ресурсы стоят по-разному. Запрос к /feed почти бесплатный, /export/csv на миллион записей — дорогой. Логично ставить разные лимиты: 1000 запросов в минуту на лёгкие эндпоинты и 10 на тяжёлые.

На практике обычно комбинируют: глобальный лимит по IP + лимит по API-ключу + отдельные лимиты на дорогие операции.

HTTP-заголовки: как сообщить клиенту о лимитах

Rate limiting без обратной связи — плохой UX. Клиент должен знать, сколько запросов ему осталось и когда лимит сбросится.

Стандарт де-факто — заголовки RateLimit-* (RFC 6585 и более новый дraft IETF):

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1713700800
Retry-After: 30
  • X-RateLimit-Limit — максимальный лимит
  • X-RateLimit-Remaining — сколько запросов осталось в текущем окне
  • X-RateLimit-Reset — unix timestamp, когда счётчик сбросится
  • Retry-After — через сколько секунд можно повторить (появляется только при 429)

Клиент с нормальной реализацией будет следить за Remaining и замедляться заранее, не дожидаясь 429. Это называется adaptive throttling и сильно снижает количество реальных отказов.

Всегда возвращай 429 Too Many Requests, а не 503 Service Unavailable — это разные ситуации, и клиентский код должен их различать.

Инструменты и реализация

Nginx

Для простых случаев Nginx справляется без дополнительных зависимостей:

http {
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    server {
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
        }
    }
}

burst=20 — разрешает всплеск до 20 дополнительных запросов. nodelay — не ставит их в очередь, а обрабатывает сразу (в рамках burst). Без nodelay лишние запросы будут ждать, что увеличивает латентность.

Redis + application layer

Для более гибкого контроля — реализуй rate limiting прямо в приложении с Redis как хранилищем состояния. Библиотеки есть для любого стека:

  • Node.js: rate-limiter-flexible, express-rate-limit + Redis store
  • Python: slowapi для FastAPI, django-ratelimit
  • Go: golang.org/x/time/rate для in-memory, go-redis/redis_rate для Redis

Пример с rate-limiter-flexible в Node.js:

const { RateLimiterRedis } = require('rate-limiter-flexible');

const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'rl_api',
  points: 100,        // запросов
  duration: 60,       // за 60 секунд
  blockDuration: 60,  // блокировать на 60 сек при превышении
});

app.use(async (req, res, next) => {
  try {
    const key = req.headers['x-api-key'] || req.ip;
    const result = await rateLimiter.consume(key);
    res.set('X-RateLimit-Remaining', result.remainingPoints);
    next();
  } catch (rejRes) {
    res.set('Retry-After', Math.ceil(rejRes.msBeforeNext / 1000));
    res.status(429).json({ error: 'Too many requests' });
  }
});

API Gateway

Если у тебя микросервисы или несколько API — выносить rate limiting в API Gateway (Kong, AWS API Gateway, Traefik) значительно удобнее, чем дублировать логику в каждом сервисе. Настройка через конфиг, централизованная аналитика, не нужно трогать код приложений.

Типичные ошибки

Слишком жёсткие лимиты без понимания реальных паттернов. Прежде чем выставлять цифры — посмотри на реальную статистику запросов. Медиана, 95-й перцентиль, максимум. Лимит должен отрезать аномалии, не мешая нормальным пользователям.

Один лимит для всех. Запрос к /ping и запрос к /reports/generate — разные по стоимости операции. Одно число для обоих — либо слишком мягко для тяжёлых операций, либо слишком жёстко для лёгких.

Нет обратной связи клиенту. Если клиент не знает о лимитах — он будет ретраить немедленно, создавая ещё больше нагрузки. Всегда возвращай Retry-After.

Счётчики в памяти без синхронизации. Если у тебя несколько инстансов приложения, in-memory счётчики не работают — каждый инстанс считает независимо, и реальный лимит умножается на количество серверов. Нужен общий стор — Redis или аналог.

Игнорирование белых списков. Собственные сервисы (мониторинг, CI/CD, административные задачи) должны быть в whitelist. Получить 429 от своего же health-check — неприятный сюрприз.

Разные лимиты для разных тарифов

Если строишь API как продукт — rate limiting становится частью бизнес-логики:

Тариф Запросов в минуту Запросов в день
Free 60 1 000
Starter 600 50 000
Pro 6 000 500 000
Enterprise без ограничений по договору

Лимиты привязываются к API-ключу, который связан с тарифом в базе данных. При апгрейде — просто обновляется запись, ничего в коде менять не нужно.

Важный нюанс: разные лимиты на разных временных горизонтах дополняют друг друга. Минутный лимит защищает от всплесков, дневной — от систематического злоупотребления.

Мониторинг и аналитика

Rate limiting без мониторинга — вслепую. Нужно отслеживать:

  • Количество 429 ответов в разрезе клиентов и эндпоинтов
  • Кто чаще всего упирается в лимиты — это или баг в клиентском коде, или потенциальная атака
  • Тренды: если количество 429 растёт — либо лимиты слишком жёсткие, либо нагрузка реально растёт

Графана с Prometheus или любой другой стек метрик. Алерт на всплеск 429 за короткий период — хороший сигнал о возможной атаке.

При проектировании API в REEXY мы закладываем rate limiting с первого дня — особенно для проектов, где API открыт для внешних интеграций или мобильных клиентов. Дешевле предусмотреть это архитектурно, чем переделывать под нагрузкой.

Что в итоге

Rate limiting — не опциональная фича, а базовая гигиена для любого API, который живёт в реальном мире. Несколько практических выводов:

  • Начни с простого: Nginx limit_req или middleware на уровне приложения закрывают большинство задач
  • Используй Redis, если у тебя несколько инстансов
  • Всегда отдавай заголовки с оставшимися лимитами — клиентам это очень помогает
  • Разные эндпоинты — разные лимиты
  • Смотри на 429-статистику регулярно, там много полезного об аномальном поведении

Rate limiting не остановит целенаправленную DDoS-атаку с тысячи IP — для этого нужны другие инструменты. Но от случайных перегрузок, багов в клиентском коде и простых злоупотреблений — защитит надёжно.