Kravchenko

Web Lab

АудитБлогКонтакты

Kravchenko

Web Lab

Разрабатываем сайты и автоматизацию на современных фреймворках под ключ

Услуги
ЛендингМногостраничныйВизитка
E-commerceБронированиеПортфолио
Навигация
БлогКонтактыАудит
Обратная связь
+7 921 567-11-16
info@kravlab.ru
с 09:00 до 18:00

© 2026 Все права защищены

•

ИП Кравченко Никита Владимирович

•

ОГРНИП: 324784700339743

Политика конфиденциальности

Рейт‑лимитинг и квоты: как защитить API от ботов, сгладить пики и держать расходы под контролем

Разработка и технологии15 января 2026 г.
Резкие всплески трафика и злоупотребления API обходятся бизнесу дорого: растут счета за инфраструктуру, падает SLA, страдают честные клиенты. Разбираем, как внедрить ограничение частоты запросов и квоты: выбор алгоритма, честные заголовки для клиентов, атомарная реализация на Redis, распределённые лимиты между датацентрами, наблюдаемость и чек‑лист запуска в прод.
Рейт‑лимитинг и квоты: как защитить API от ботов, сгладить пики и держать расходы под контролем

Оглавление

  • Зачем бизнесу ограничение частоты
  • Виды лимитов: где и на кого ставить
  • Алгоритмы: фиксированное окно, скользящее окно, токен‑бакет, протекающее ведро
  • Практика: токен‑бакет на Redis с Lua
    • Lua‑скрипт для атомарного потребления токенов
    • Python‑пример использования
  • Заголовки и UX: как корректно сказать «попробуйте позже»
  • Квоты по тарифам и биллинг: суточные/месячные окна
  • Распределённые лимиты в нескольких датацентрах
  • Наблюдаемость, алерты и тестирование
  • Частые ошибки и анти‑паттерны
  • Чек‑лист внедрения

Зачем бизнесу ограничение частоты

  • Защита от ботов и «шумных» клиентов. Один неверно настроенный интегратор способен утопить ваш бэкенд в запросах, выдавить честных пользователей и выбить SLA.
  • Сглаживание пиков. Когда все клиенты стартуют задачи «ровно в 00
    » или после рассылки, серверы получают волну. Лимиты распределяют нагрузку во времени, уменьшая p95/p99 задержек.
  • Контроль затрат. Без лимитов облачный провайдер рад взять деньги за лишние ядра и трафик. Прозрачные границы помогают планировать и не переплачивать.
  • Честность и предсказуемость. Клиенты ценят понятные правила: сколько можно, что будет при превышении и когда можно повторить.

Итого: ограничение частоты — это не «досадная блокировка», а механизм экономии и стабилизации сервиса.

Виды лимитов: где и на кого ставить

Стоит мыслить слоями:

  • Глобальные лимиты — на весь API/кластер. Нужны как предохранитель.
  • Пер‑клиент (по API‑ключу/аккаунту/тенанту) — основной уровень справедливости.
  • Пер‑IP/ASN — помогает против анонимных пиков и ошибок конфигурации.
  • Пер‑эндпоинт/метод — разные стоимости: чтение профиля vs расчёт сложного отчёта.
  • Пер‑ресурс/«вес» — один запрос может «стоить» 1, другой — 10 «кредитов».

Комбинируйте: «60 rps на ключ, но не более 10 rps на /search и 2 rps на /export».

Алгоритмы: фиксированное окно, скользящее окно, токен‑бакет, протекающее ведро

  • Фиксированное окно (fixed window). Счётчик в окне (например, 60 сек). Плюсы: просто. Минусы: пограничные «двойные» всплески на стыке окон.
  • Скользящее окно (sliding window). Учитывает точные метки времени. Плюсы: честнее. Минусы: сложнее хранить/подсчитывать.
  • Токен‑бакет (token bucket). В «ведро» капают токены с фиксированной скоростью; каждый запрос забирает N токенов. Можно делать «всплески» до объёма ведра. Хорош для реального мира и пользователен к кратковременным пикам.
  • Протекающее ведро (leaky bucket). Стабильный отток; очереди выравнивают поток. Удобно для «полосы пропускания» с жёстким сглаживанием.

Для API чаще всего подходит токен‑бакет: он прост, быстр и гибко настраивается.

Практика: токен‑бакет на Redis с Lua

Почему Redis:

  • Быстро. Операции O(1).
  • Атомарно. Lua‑скриптом одновременно пополняем и списываем токены.
  • TTL. Ключ «самоуничтожается», если клиент перестал стучаться.

Схема ключей: rate:{scope}:{id}:{route}

Где:

  • scope — user, ip, tenant, global и т. п.
  • id — конкретный идентификатор (например, user_123).
  • route — «весовой» эндпоинт или wildcard.

Значение — хэш с полями tokens (остаток) и ts (последнее обновление в мс).

Lua‑скрипт для атомарного потребления токенов

Скрипт: пополняет ведро по прошедшему времени, списывает токены за запрос, выставляет TTL и возвращает остаток и время до следующего токена.

-- token_bucket.lua
-- KEYS[1] - ключ ведра
-- ARGV[1] - capacity (максимум токенов)
-- ARGV[2] - refill_rate (токенов в секунду)
-- ARGV[3] - cost (стоимость запроса в токенах)
-- ARGV[4] - ttl_seconds (время жизни ключа)
-- Возвращает: {allowed (0/1), tokens_after, retry_after_ms}

local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local cost = tonumber(ARGV[3])
local ttl = tonumber(ARGV[4])

-- Текущее время Redis в мс
local now_data = redis.call('TIME')
local now = tonumber(now_data[1]) * 1000 + math.floor(tonumber(now_data[2]) / 1000)

local data = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(data[1])
local ts = tonumber(data[2])

if tokens == nil then
  tokens = capacity
  ts = now
else
  if now > ts then
    local elapsed_ms = now - ts
    local refill = (elapsed_ms / 1000.0) * rate
    tokens = math.min(capacity, tokens + refill)
  end
end

local allowed = 0
local retry_after_ms = 0

if tokens >= cost then
  tokens = tokens - cost
  allowed = 1
else
  local deficit = cost - tokens
  -- сколько мс нужно, чтобы накопить дефицит
  if rate > 0 then
    retry_after_ms = math.ceil((deficit / rate) * 1000)
  else
    retry_after_ms = 2^31 - 1
  end
end

-- Сохраняем состояние
redis.call('HMSET', key, 'tokens', tokens, 'ts', now)
redis.call('EXPIRE', key, ttl)

return {allowed, tokens, retry_after_ms}

Python‑пример использования

В примере ограничим пользователя до 60 rps с «вёдром» на 120 токенов (может «взорваться» до 120 сразу), стоимость запроса — 1 токен. Для тяжёлого маршрута можно ставить cost=5.

# requirements: redis>=5.0.0
import time
import math
from redis import Redis

redis_client = Redis(host='localhost', port=6379, decode_responses=True)

with open('token_bucket.lua', 'r', encoding='utf-8') as f:
    LUA_SCRIPT = f.read()

script = redis_client.register_script(LUA_SCRIPT)

def allow(scope: str, ident: str, route: str,
          capacity: int = 120,
          refill_rate: float = 60.0,
          cost: int = 1,
          ttl_seconds: int = 3600):
    key = f"rate:{scope}:{ident}:{route}"
    allowed, tokens_after, retry_after_ms = script(keys=[key], args=[capacity, refill_rate, cost, ttl_seconds])
    return int(allowed) == 1, float(tokens_after), int(retry_after_ms)

# Пример: проверка перед обработкой запроса
user_id = 'user_42'
route = 'GET:/search'

ok, tokens, retry_ms = allow('user', user_id, route, capacity=120, refill_rate=60.0, cost=1, ttl_seconds=900)
if not ok:
    # Возвращаем 429 с корректными заголовками
    retry_after = math.ceil(retry_ms / 1000)
    print(f"429 Too Many Requests. Retry-After: {retry_after}s")
else:
    # Обрабатываем запрос
    print(f"OK, tokens left: {tokens:.2f}")

Альтернатива «на входе» — базовая защита на уровне NGINX/Ingress. Это не заменяет бизнес‑лимиты, но даёт дешёвый барьер против шумных IP.

# nginx.conf (фрагмент)
# 100 запросов в секунду на IP, с «взрывом» до 200 (burst)
limit_req_zone $binary_remote_addr zone=perip:10m rate=100r/s;

server {
  location /api/ {
    limit_req zone=perip burst=200 nodelay;
    proxy_pass http://backend;
  }
}

Заголовки и UX: как корректно сказать «попробуйте позже»

Отдаём 429 Too Many Requests и подсказываем клиенту, когда повторить. Используйте стандартные поля RateLimit из RFC 9333:

  • RateLimit-Limit — лимит и окно, например «60;w=1» (60 запросов в 1 секунду) или с политиками для разных маршрутов.
  • RateLimit-Remaining — сколько ещё можно в текущем окне/по текущему bucket.
  • RateLimit-Reset — через сколько секунд лимит восстановится (целое или дробное), округляйте вверх.
  • Retry-After — когда можно повторить. Подходит и при 503/429.

Пример ответа:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
RateLimit-Limit: 60;w=1
RateLimit-Remaining: 0
RateLimit-Reset: 0.8
Retry-After: 1

{"error":"rate_limited","message":"Превышен лимит. Повторите запрос через ~1 сек."}

Для квот (месячных лимитов) вместо секунд ставьте понятный текст в теле, а заголовками давайте оставшийся баланс «кредитов», например X-Quota-Remaining (если не готовы к полям из RFC, но лучше придерживаться стандарта, когда возможно).

Совет: не ломайте UX. Если операция безопасна для повтора — подсказывайте это. Для небезопасных — используйте очереди и асинхронные задачи, а клиенту возвращайте 202 Accepted.

Квоты по тарифам и биллинг: суточные/месячные окна

Часто нужен не только «rps‑ограничитель», но и квоты: N запросов в сутки/месяц по тарифу.

  • Окна: календарные (месяц по календарю) или скользящие (последние 30 дней). Календарные проще — достаточно счётчика в базе и CRON‑сброса.
  • Модель учёта: «кредиты». Тяжёлым операциям назначайте цену выше.
  • Мягкий и жёсткий предел: сначала предупреждаем и снижаем скорость (throttle), потом полностью блокируем сверх тарифного плана.
  • Коммуникация: e‑mail/вебхук/уведомление в кабинете при достижении 80/90/100% квоты.

Технически: храните агрегаты в БД (PostgreSQL) с индексом по (tenant_id, period). Пополнение/списание — транзакции с идемпотентностью (ид операции), чтобы не списать дважды. Для онлайновой проверки при каждом запросе — кэшируйте в Redis, но источником истины держите БД.

Распределённые лимиты в нескольких датацентрах

Если у вас несколько инстансов/регионов, есть три подхода:

  1. Централизованный Redis/Upstash/MemoryStore с низкой латентностью. Просто, но добавляет межрегионную задержку и точку отказа (решается репликацией и отказоустойчивыми кластерами).

  2. Локальные лимиты + «мягкая» консистентность. Каждый регион держит свой Redis и лимитирует до доли от глобального лимита (например, 50/50). Подходит при независимых потоках.

  3. Алгоритмы без координации (CRDT‑счётчики/сквозное хэширование клиентов к «ведру‑шарду»). Сложнее, но надёжнее на больших масштабах.

Рекомендация для 95% случаев: шардируйте ключи по консистентному хэшу на Redis‑кластер, включите репликацию и автоматический фейловер. На случай деградации — локальный «предохранитель» в процессе (in‑memory) с консервативными ограничениями, чтобы не улететь в безлимит.

Наблюдаемость, алерты и тестирование

Метрики:

  • rate_limiter_requests_total{scope,route,decision="allow|deny"}
  • rate_limiter_retry_after_ms{route} (квантили p50/p95/p99)
  • tokens_gauge{route} — усреднённый остаток токенов по горячим ключам
  • доля 429 от общего трафика по маршрутам

Логи/трассировки:

  • Коррелируйте 429 с маршрутом и tenant_id.
  • Добавляйте в трассировки поля decision=deny и retry_after_ms.

Тесты:

  • Нагрузочные: ступенчатый рост RPS, проверка, что p95 стабилизируется, а доля 429 ожидаема.
  • Границы окон: массовый старт в начале и конце окна — без «двойных» пиков.
  • Отказоустойчивость: отключение Redis, проверка деградации к локальному лимиту и корректных ответов.

Частые ошибки и анти‑паттерны

  • Считать только по IP. NAT и мобильные сети приведут к ложным срабатываниям. Нужны уровни: ключ/tenant/IP совместно.
  • Жёстко резать всё одинаково. Разные маршруты стоят по‑разному. Вводите «стоимость» (cost) запроса в токенах.
  • Обнулять счётчик ровно «в 00
    ». Это создаёт волну. Используйте токен‑бакет или джиттер на сбросе.
  • Прятать лимиты. Клиенты ненавидят сюрпризы: документируйте, отправляйте заголовки RateLimit‑* и предупреждения.
  • Наказывать за ошибки сервера. 5xx и таймауты не должны съедать «кредиты» клиента.
  • Игнорировать повторные безопасные запросы. Если клиент корректно использует идемпотентные ключи — не снимайте второй раз «стоимость» операции (для биллинга/квот).

Чек‑лист внедрения

  • Определите цели: защита, сглаживание пиков, биллинг.
  • Сегментируйте лимиты: глобальный, пер‑tenant, пер‑IP, пер‑маршрут.
  • Выберите алгоритм: для API — токен‑бакет с «стоимостью» запросов.
  • Реализуйте быстрый слой на Redis + Lua (атомарность) и «предохранитель» в процессе.
  • Пропишите заголовки RateLimit‑Limit/Remaining/Reset и Retry‑After. Задокументируйте.
  • Настройте наблюдаемость: метрики allow/deny, доля 429, retry_after_ms, дашборды и алерты.
  • Добавьте квоты в БД для тарифов, уведомления при 80/90/100%.
  • Проведите нагрузочные тесты и «испытание отказом» Redis.
  • Введите мягкие лимиты сначала (лог‑only/варнинг‑режим), затем — enforce.
  • Регулярно пересматривайте параметры по данным: сезонность, пиковые часы, новые роуты.

Итог: хорошо настроенный рейт‑лимитинг и квоты снижают риски инцидентов, улучшают предсказуемость ответов и экономят бюджет, при этом оставаясь прозрачными и честными для клиентов.


Redisrate limitingAPI безопасность