Kravchenko

Web Lab

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

Kravchenko

Web Lab

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

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

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

•

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

•

ОГРНИП: 324784700339743

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

Лимиты запросов и квоты в API: защита от пиков, предсказуемые SLA и экономия бюджета

Разработка и технологии10 апреля 2026 г.
Лимитирование запросов и квотирование помогают удерживать стабильность сервиса, предсказуемость SLA и расходы инфраструктуры под контролем. В статье — где ставить лимиты, какие алгоритмы использовать, как вернуть корректный ответ клиенту и связать всё это с тарифами, наблюдаемостью и биллингом.
Лимиты запросов и квоты в API: защита от пиков, предсказуемые SLA и экономия бюджета

  • Оглавление
    • Зачем бизнесу лимиты и квоты
    • Где ставить лимиты: периметр, шлюз, приложение, база
    • Что именно ограничиваем: субъект, ресурс, «вес» запроса
    • Алгоритмы: фиксированное окно, скользящее окно, ведро с жетонами, «протекающее ведро»
    • Примеры: NGINX на периметре; Redis + Lua + Go внутри сервиса
    • Корректные ответы клиенту и UX
    • Наблюдаемость: метрики, логи, алерты
    • Квоты и монетизация
    • Тестирование и проверка на бою
    • Частые ошибки и чек-лист внедрения

Зачем бизнесу лимиты и квоты

Лимиты запросов (rate limiting) и квоты — это не про «мешать пользователям», а про здоровье сервиса и деньги. Они:

  • защищают от внезапных пиков, не давая одному клиенту «съесть» весь ресурс;
  • стабилизируют время ответа и сокращают инциденты при всплесках;
  • делают расходы на инфраструктуру предсказуемыми и управляемыми;
  • упрощают тарифы: можно честно обещать SLA и масштабировать выручку без бесконечного оверинжиниринга.

Признаки, что пора внедрять лимиты:

  • редкие, но разрушительные пики нагрузки (акции, интеграции партнёров, массовые ретраи);
  • «шум» от одной группы клиентов бьёт по остальным;
  • непредсказуемые счета за облако из‑за автоскейлинга;
  • служба поддержки ловит жалобы «вчера работало быстро, сегодня всё тормозит».

Где ставить лимиты: периметр, шлюз, приложение, база

Правильный ответ обычно «в нескольких местах», но с разными задачами и деталями.

  • На периметре (CDN/WAF/балансировщик). Быстрый и дешёвый отсев по IP, ASN, стране, бот‑признакам. Полезно против перегрева от внешних источников. Подходит для простых правил: «не более 10 запросов/с с одного IP».
  • В API‑шлюзе/прокси (Kong, Envoy, NGINX). Тонкие лимиты по ключу клиента, маршруту, методу, «весу» запроса. Хорошее место, если у вас много бэкендов и нужна единая политика.
  • В приложении. Здесь видно бизнес‑контекст: клиент, организация, тариф, стоимость операции. Можно учитывать нагрузку на зависимые системы и выносить «дорогие» запросы в отдельные корзины.
  • У хранилищ и внешних интеграций. Лимиты на пул подключений к базе, на частоту запросов к внешним API, чтобы чужие лимиты не выбивали ваш SLA.

Золотое правило: дешёвые фильтры — как можно раньше; самые умные и «дорогие» — там, где есть бизнес‑контекст.

Что именно ограничиваем: субъект, ресурс, «вес» запроса

  • Субъект лимита: ключ API, пользователь, организация (тенант), IP, токен скоупа. Часто применяют каскад: «сначала по ключу, затем по организации».
  • Ресурс: путь или группа маршрутов (например, /search отдельно от /billing), метод (GET/POST), отдельные «дорогие» операции.
  • «Вес» запроса: не все запросы равны. Поиск по большим индексам, сложные отчёты или генерация файлов могут обходиться в 5–50 раз дороже, чем «получить профиль». Весовой лимит позволяет считать «жетоны» пропорционально стоимости.
  • Тип лимита:
    • скорость: X запросов в секунду/минуту (с «всплеском»);
    • квота за период: Y операций в сутки/месяц;
    • конкурентность: не более Z одновременных запросов/задач;
    • бюджет ретраев: ограничить шквал повторов от неудачных интеграций.

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

Фиксированное окно (fixed window)

Просто и быстро: считаем запросы в минуте и обнуляем счётчик в начале новой минуты. Плюсы — легко реализовать в Redis (INCR + EXPIRE). Минус — пограничные «качели»: клиент может сделать почти двойной объём на стыке окон.

Скользящее окно (sliding window)

Гладкая нагрузка: учитываем последние N секунд. Реализации:

  • «два окна»: текущая и предыдущая минуты с линейной интерполяцией — почти бесплатно, достаточно двух счётчиков;
  • «окно с журналом» на отсортированном множестве (Redis ZSET), где удаляем старые метки времени — точнее, но дороже по памяти и времени.

Ведро с жетонами (token bucket)

Подходит, когда нужен «всплеск» (burst), но средняя скорость ограничена. Ведро пополняется жетонами с постоянной скоростью; запрос тратит жетоны по своему «весу». Если жетонов нет — отклоняем или ждём. Можно хранить состояние в Redis и обновлять атомарным Lua‑скриптом.

Протекающее ведро (leaky bucket)

Стабильный «слив» с фиксированной скоростью. Удобно, если главное — равномерный поток. Минус — хуже поддержка «всплеска».

Практика: для API чаще берут token bucket или «два окна». Для экспортов/отчётов — квоты за период и/или очереди задач.

Примеры: NGINX на периметре; Redis + Lua + Go внутри сервиса

NGINX: простой лимит по IP и по ключу клиента

Этот вариант дешёв и эффективен у границы. Он не знает о тарифах, но срежет грубые пики.

# Лимит по IP: 5 запросов/с, допускаем всплеск до 10 без задержки
limit_req_zone $binary_remote_addr zone=per_ip:10m rate=5r/s;

# Лимит по ключу API из заголовка X-API-Key: 100 запросов/мин
map $http_x_api_key $api_key {
  default $http_x_api_key;
}
limit_req_zone $api_key zone=per_key:20m rate=100r/m;

server {
  listen 443 ssl;

  location /api/ {
    # Сначала отсев по IP
    limit_req zone=per_ip burst=10 nodelay;

    # Затем — по ключу клиента
    limit_req zone=per_key burst=50 nodelay;

    # Полезные заголовки клиенту
    add_header X-RateLimit-Policy "per_ip=5r/s; per_key=100r/m" always;

    proxy_pass http://backend;
  }
}

Плюс: быстро и просто. Минус: нет знания о «весе» запроса и тарифах. Часто комбинируют с более умным лимитом в приложении.

Redis + Lua + Go: ведро с жетонами с учётом «веса» запросов

Атомарный Lua‑скрипт хранит состояние ведра (сколько жетонов, когда последний раз пополняли). Приложение передаёт ключ (клиент/тенант), вместимость ведра, скорость пополнения и «вес» текущего запроса.

Lua‑скрипт для Redis:

-- token_bucket.lua
-- KEYS[1] = ключ ведра (например, rate:{tenant_id})
-- ARGV[1] = capacity (вместимость)
-- ARGV[2] = refill_per_sec (жетонов в секунду)
-- ARGV[3] = now_ms (текущее время в мс)
-- ARGV[4] = cost (стоимость текущего запроса в жетонах)
-- ARGV[5] = ttl_sec (сколько держать состояние, чтобы не разрастался ключспейс)

local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_per_sec = tonumber(ARGV[2])
local now_ms = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local ttl_sec = tonumber(ARGV[5])

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_ms
end

-- Сколько жетонов добавить с последнего обновления
local delta_ms = now_ms - ts
if delta_ms < 0 then delta_ms = 0 end
local add = (delta_ms / 1000.0) * refill_per_sec

-- Обновляем уровень с учётом вместимости
tokens = math.min(capacity, tokens + add)

local allowed = 0
if tokens >= cost then
  tokens = tokens - cost
  allowed = 1
end

redis.call('HMSET', key, 'tokens', tokens, 'ts', now_ms)
redis.call('EXPIRE', key, ttl_sec)

-- Возвращаем: allowed, оставшиеся жетоны (округлим вниз для стабильности)
return {allowed, math.floor(tokens)}

Пример на Go с github.com/redis/go-redis/v9:

package main

import (
	"context"
	"fmt"
	"time"

	redis "github.com/redis/go-redis/v9"
)

var script = redis.NewScript(`
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_per_sec = tonumber(ARGV[2])
local now_ms = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local ttl_sec = tonumber(ARGV[5])

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_ms
end
local delta_ms = now_ms - ts
if delta_ms < 0 then delta_ms = 0 end
local add = (delta_ms / 1000.0) * refill_per_sec
tokens = math.min(capacity, tokens + add)
local allowed = 0
if tokens >= cost then
  tokens = tokens - cost
  allowed = 1
end
redis.call('HMSET', key, 'tokens', tokens, 'ts', now_ms)
redis.call('EXPIRE', key, ttl_sec)
return {allowed, math.floor(tokens)}
`)

func allow(ctx context.Context, rdb *redis.Client, key string, capacity, refillPerSec, cost, ttlSec int) (bool, int, error) {
	res, err := script.Run(ctx, rdb, []string{key}, capacity, refillPerSec, time.Now().UnixMilli(), cost, ttlSec).Result()
	if err != nil {
		return false, 0, err
	}
	vals := res.([]interface{})
	allowed := vals[0].(int64) == 1
	remaining := int(vals[1].(int64))
	return allowed, remaining, nil
}

func main() {
	ctx := context.Background()
	rdb := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})
	defer rdb.Close()

	key := "rate:org:42"
	capacity := 200       // максимум жетонов в ведре
	refillPerSec := 50    // скорость пополнения
	cost := 5             // вес операции
	ttlSec := 3600        // хранить час

	ok, rem, err := allow(ctx, rdb, key, capacity, refillPerSec, cost, ttlSec)
	if err != nil {
		panic(err)
	}
	fmt.Printf("allowed=%v, remaining=%d\n", ok, rem)
}

Как связать с тарифом: храните параметры ведра (capacity, refill) в профиле тенанта, а «вес» операции — в справочнике маршрутов. Например, «чтение профиля» = 1, «генерация отчёта» = 20.

Корректные ответы клиенту и UX

  • Статус 429 Too Many Requests. Текст — человеческий: что произошло и через сколько повторить.
  • Заголовки:
    • Retry-After — подскажите секунды ожидания (или время в формате даты);
    • X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset — де‑факто стандарт, понятный многим клиентам;
    • или поля семейства RateLimit-*, если ваша платформа их поддерживает.
  • Не душите «тихие» запросы: давайте минимальный бесплатный лимит, чтобы интеграции не падали сразу и могли деградировать грациозно.
  • Для квот за период — заранее предупреждайте (почта/веб‑хуки), когда израсходовано 80/90/100%.

Мини‑пример ответа:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 15
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1712678400

{"error":"rate_limited","message":"Превышен лимит: попробуйте через 15 секунд или обратитесь в поддержку для повышения квоты."}

Наблюдаемость: метрики, логи, алерты

  • Метрики:
    • rate_limited_total{key, route, reason}
    • remaining_tokens_gauge{tenant}
    • обработанные/отклонённые запросы по субъектам и маршрутам
  • Логи: ключ клиента, маршрут, рассчитанный «вес», оставшиеся жетоны, причина отклонения.
  • Дашборды: распределение отказов по тарифам, карта «горячих» маршрутов.
  • Алерты: резкий рост 429, исчерпание квот у ключевых клиентов, всплеск ретраев.

Квоты и монетизация

  • Прозрачные тарифы: «10 тыс. дешёвых операций + 1 тыс. дорогих в месяц» проще объяснить, чем «безлимит». Клиент понимает, за что платит.
  • «Мягкие» и «жёсткие» лимиты: сначала замедляйте (увеличивайте задержки или снижайте приоритет), затем — отказывайте. Это уменьшает негатив и сохраняет SLA для остальных.
  • Сверхлимиты: дайте возможность докупать пакет операций. Автоматический апгрейд при согласии клиента снижает отток.
  • Самообслуживание: страница «Мои лимиты» с графиками расхода и кнопкой «Увеличить» экономит часы поддержки.

Тестирование и проверка на бою

  • Нагрузочное тестирование: сценарии «пила», «ступенька», «шквал ретраев». Проверяйте, что средняя скорость выдерживается, а всплески ограничиваются.
  • Трафик‑тени: применяйте лимиты в «сухом режиме» (только считаем, не блокируем) и сравнивайте метрики.
  • Интеграции с партнёрами: предупредите о новых лимитах, дайте тестовый стенд и образцы ответов 429.
  • Каос‑тесты: что будет, если Redis недоступен? Предусмотрите режим «fail-open» или «fail-closed» по типам маршрутов. К критичным (платёж) — одно, к вспомогательным (поиск) — другое.

Частые ошибки и как их избежать

  • «Один глобальный лимит на всё». В итоге шумный маршрут душит критичные операции. Лечится разделением по маршрутам и «весу».
  • Только в памяти на инстансе. При горизонтальном масштабировании лимиты перестают быть глобальными. Если нужна общая политика — используйте Redis/шлюз.
  • Срыв на границах окон. Фиксированное окно провоцирует двойные пики. Используйте «два окна» или token bucket.
  • Отсутствие обратной связи. Без заголовков и Retry-After клиенты слепы и бомбят ретраями. Дайте подсказки и примеры экспоненциальной паузы.
  • Бесконечные EXPIRE без контроля. Держите состояние не дольше разумного TTL, чтобы не разрастался ключспейс Redis.
  • Игнор времени сервера. Используйте время Redis (команда TIME) или передавайте время с бэкенда, где вы контролируете синхронизацию.

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

  • Определите субъекты и ресурсы лимита: кто и что ограничивается.
  • Разбейте операции по «весу». Согласуйте со службой поддержки и продуктовой командой.
  • Выберите места применения: периметр для грубого отсечения, шлюз/приложение — для умных правил.
  • Подберите алгоритм: token bucket для «всплесков», «два окна» для простоты.
  • Продумайте отказоустойчивость: что делать, если хранилище лимитов недоступно.
  • Возвращайте корректные ответы: 429, Retry-After, X-RateLimit-*.
  • Включите наблюдаемость: метрики, логи, алерты, дашборды.
  • Сверьте с тарифами и биллингом. Настройте уведомления об исчерпании.
  • Запустите в «сухом режиме», затем постепенно включайте блокировку.
  • Проведите учения с поддержкой: ответы на типовые вопросы клиентов.

Итоги

Лимитирование запросов и квотирование — это управляемая производительность и предсказуемые деньги. Начните с простого: грубые лимиты на периметре и осознанные правила в приложении с учётом «веса» операций. Добавьте корректные ответы, наблюдаемость и связь с тарифами — и у вас будет система, которая защищает сервис, помогает продажам и снижает риски инцидентов без боли.


APIлимитыквоты