Прямой ответ
Rate limiting — обязательный слой любого API, доступного извне: без него первый же скрипт-переборщик (подбор паролей, скрейпинг, «случайный» бесконечный цикл у клиента) превращается в инцидент. Практическая формула внедрения: алгоритм — token bucket или sliding window (позволяют редкие всплески, в отличие от жёсткого fixed window); ключ лимита — аутентифицированный пользователь/API-ключ, для анонимных — IP (с пониманием NAT: офис за одним IP — не злоумышленник, лимит по IP щедрее или по комбинации); уровни — грубый лимит на входе (nginx/CDN — от явного злоупотребления), точные лимиты в приложении (по пользователям и операциям: login строже чтения, дорогие операции отдельно); ответ — 429 с заголовками Retry-After и остатком лимита (клиент должен понимать, когда вернуться, — RFC 6585). И отдельная категория, которую забывают: лимиты на аутентификацию (попытки логина, отправка кодов/SMS — самые дорогие для бизнеса и самые атакуемые эндпоинты) — строже общих на порядок.
Алгоритмы: fixed window, sliding, token bucket
Fixed window (N запросов в минуту, счётчик обнуляется) — прост (INCR+EXPIRE в Redis), но граница окна даёт всплеск 2N на стыке двух окон; приемлем для грубых лимитов. Sliding window (скользящее окно или log/count) — убирает стыковой всплеск, чуть дороже; хороший дефолт для пользовательских лимитов. Token bucket — токены накапливаются с постоянной скоростью, запрос тратит токен, ведро ограничивает всплеск: естественно описывает «средняя скорость X, всплеск до Y» — стандарт для API-гейтвеев (реализации: nginx limit_req — leaky bucket-вариант, библиотеки slowapi/limits в Python, Envoy rate limit service). Выбор на практике: для 90% случаев достаточно «sliding window в приложении + грубый limit_req в nginx»; тонкости алгоритмов важны, когда лимит — часть продукта (тарифы API с явными burst-параметрами). Хранилище счётчиков — Redis (паттерны кеша — та же инфраструктура) с деградацией «fail open» (Redis недоступен — лимиты мягко отключаются, доступность важнее точности лимита; для критичных лимитов — наоборот, документируйте решение).
Ключи лимита и уровни обороны
По кому считать: пользователь (по токену — честно для аутентифицированных), API-ключ (для партнёрских интеграций — лимит как часть контракта), IP (для анонимных; за прокси/CDN — реальный IP из X-Forwarded-For, настроенного доверенно — nginx-прокси передаёт его правильно, иначе все клиенты «из-под одного IP»), комбинация user+endpoint (лимит «5 попыток логина в минуту» не мешает чтению). Уровни: CDN/edge (Cloudflare-подобные — от DDoS и ботов, до приложения не доходит), nginx (limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m; limit_req zone=api burst=50 nodelay — грубая защита приложения от перегрузки), приложение (точные бизнес-лимиты: тарифы, антифрод), база (пул соединений — последний рубеж от исчерпания). Дифференциация по операциям: чтение — щедро (60–300/мин), запись — умеренно, аутентификация/восстановление/платежи — единицы в минуту с прогрессивной блокировкой (5 провалов — капча/блокировка на 15 минут); дорогие операции (генерация отчёта, экспорт) — отдельный маленький лимит + очередь вместо синхронного исполнения.
Ответ 429 и контракт с клиентом
Лимит без понятного ответа — генератор саппорт-тикетов: клиент не понимает, что произошло, и ретраит агрессивнее. Стандарт ответа: 429 Too Many Requests (или 403 для злонамеренного перебора) с Retry-After: 30 (секунды или дата — когда повторить); информативные заголовки остатка — X-RateLimit-Limit/X-RateLimit-Remaining/X-RateLimit-Reset (де-факто стандарт, GitHub API — эталонный пример): клиент может расходовать лимит осознанно и не упираться вслепую. Документация лимитов — часть API-контракта (какие лимиты, как расширить, как читается 429). Поведение при получении 429 у клиента — экспоненциальный backoff с джиттером (ретрай «через секунду» в цикле продлевает блокировку и нагружает сервер; рекомендация клиентам — в документации). Отдельно: 429 не должен ломать веб-интерфейс «молча» — фронт показывает «слишком много запросов, попробуйте через N секунд», а не бесконечный спиннер.
Практический сценарий: лимиты для небольшого публичного API
Продукт: API с тарифами free/pro, веб-клиент и партнёрские интеграции. Конфигурация: nginx на входе — rate=60r/m на IP с burst 120 (защита от скриптов-пылесосов; реальные пользователи не упираются). Приложение: free — 60 запросов/мин на ключ, pro — 600; заголовки RateLimit-* в каждом ответе; 429 с Retry-After. Спец-зоны: /auth/login — 5/мин на IP+логин (перебор пароля), /auth/send-code — 3/час на пользователя + дневной лимит (SMS стоит денег — антифрод-контур), /export — 2/час (тяжёлая операция уходит в фоновую очередь — Celery). Наблюдаемость: метрика 429-ответов по ключам (Grafana-дашборд) — всплеск по одному ключу означает либо баг клиента (ретрай-шторм), либо злоупотребление; алерт на аномалии. Через месяц по метрикам видно: 80% упирающихся — free-пользователи с ретрай-штормами из-за отсутствия backoff — правка документации и SDK-примера снимает половину 429 без изменения лимитов.
FAQ
Чем rate limiting отличается от DDoS-защиты? Лимиты — про справедливое распределение и злоупотребления на уровне приложения (обычный трафик, много клиентов); DDoS-защита — про объёмный трафик/ботнеты на сетевом и edge-уровне. Они дополняют: edge гасит объём, приложение дозирует легитимных клиентов.
Как лимитировать веб-приложение для людей (не API)? Мягче и по операциям: глобальные лимиты на человека обычно не нужны (человек не делает 300 запросов в минуту), а нужны точечные — логин, формы, отправка сообщений (спам), тяжёлые действия; плюс серверная защита от ботов (капча на подозрительные паттерны) и edge-уровень.
Fail open или fail closed при отказе Redis счётчиков? Для общих лимитов — fail open (Redis лёг — сервис работает без лимитов; обратное превращает отказ кеша в полный отказ продукта). Для критичных (login-перебор, платежи) — fail closed или локальный деградный лимит в памяти процесса. Решение фиксируется явно — «по умолчанию» здесь не бывает.
Считать ли лимиты до или после авторизации? До (на входе, по IP/ключу) — дешёвый слой, защищающий саму авторизацию; после — точные пользовательские лимиты. Оба: грубый входной лимит не должен требовать разбора токена, иначе атакующий грузит дорогую проверку на каждом запросе.
Источники: RFC 6585 (429), документация nginx limit_req, GitHub REST rate limits — ссылки в шапке.