Rate limiting: алгоритмы, реализация и защита API от перегрузки
Любой публичный API без rate limiting — это приглашение к злоупотреблению. Один агрессивный клиент способен положить сервис для всех остальных. Rate limiting ограничивает количество запросов от клиента за единицу времени, защищая от DDoS, brute-force атак и обеспечивая fair usage между пользователями.
Зачем rate limiting
- Защита от DDoS и abuse — ограничение скорости запросов не даёт одному клиенту исчерпать ресурсы.
- Fair usage — все пользователи получают равный доступ к API.
- Контроль затрат — предотвращение неожиданных счетов за инфраструктуру.
- Защита downstream-сервисов — API Gateway ограничивает нагрузку до того, как она дойдёт до базы данных.
Алгоритмы rate limiting
Fixed Window — простейший: считаем запросы в фиксированном окне (например, 1 минута). Проблема — burst на границе окон: 100 запросов в конце минуты + 100 в начале следующей = 200 за 2 секунды.
Sliding Window Log — храним timestamp каждого запроса, считаем попавшие в окно. Точный, но дорогой по памяти (O(n) на клиента).
Sliding Window Counter — компромисс: комбинируем счётчики текущего и предыдущего окна с весом. Точность ~99.7% при минимальной памяти.
Token Bucket — самый популярный. Корзина наполняется токенами с фиксированной скоростью. Запрос забирает токен. Нет токенов — отказ. Позволяет burst до размера корзины:
Token Bucket (capacity=10, refill=2/sec):
[||||||||||] ← 10 токенов (полная корзина)
Burst: 10 запросов мгновенно ✓
Затем: 2 запроса/сек (скорость пополнения)
Запрос → забирает 1 токен
Время → добавляет 2 токена/сек (до capacity)
Leaky Bucket — запросы попадают в очередь фиксированного размера и обрабатываются с постоянной скоростью. Сглаживает burst, но добавляет latency.
Реализация на Redis
Redis — стандартный выбор для rate limiting: атомарные операции, TTL, высокая скорость. Token bucket через Lua-скрипт:
import redis
import time
RATE_LIMIT_LUA = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(data[1]) or capacity
local last_refill = tonumber(data[2]) or now
-- Пополняем токены
local elapsed = now - last_refill
local new_tokens = math.min(capacity, tokens + elapsed * refill_rate)
if new_tokens >= 1 then
redis.call('HSET', key, 'tokens', new_tokens - 1, 'last_refill', now)
redis.call('EXPIRE', key, math.ceil(capacity / refill_rate) + 1)
return 1 -- разрешено
else
redis.call('HSET', key, 'tokens', new_tokens, 'last_refill', now)
redis.call('EXPIRE', key, math.ceil(capacity / refill_rate) + 1)
return 0 -- отклонено
end
"""
r = redis.Redis()
script = r.register_script(RATE_LIMIT_LUA)
def is_allowed(client_id: str, capacity=100, refill_rate=10) -> bool:
"""Token bucket: capacity=100, refill=10 req/sec"""
result = script(
keys=[f"ratelimit:{client_id}"],
args=[capacity, refill_rate, time.time()]
)
return bool(result)
Lua-скрипт выполняется атомарно — нет race conditions даже при тысячах конкурентных запросов.
Rate limiting в API Gateway
Nginx — встроенный модуль limit_req (leaky bucket):
# nginx.conf
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;
proxy_pass http://backend;
}
}
}
Envoy — token bucket через route-level config:
# envoy rate limit
rate_limits:
- actions:
- remote_address: {}
limit:
requests_per_unit: 100
unit: MINUTE
Kong — plugin rate-limiting с поддержкой Redis для distributed:
curl -X POST http://kong:8001/services/my-api/plugins \
--data "name=rate-limiting" \
--data "config.minute=100" \
--data "config.policy=redis" \
--data "config.redis_host=redis"
Distributed rate limiting
На нескольких инстансах возникает проблема: каждый узел видит только свою часть трафика. Решения:
- Централизованный Redis — все узлы ходят в один Redis. Просто, но Redis становится SPOF и добавляет latency на каждый запрос.
- Local + sync — каждый узел считает локально, периодически синхронизирует с Redis. Допускает кратковременное превышение лимита.
- Sliding window с Redis Cluster — шардирование по ключу клиента. Масштабируется, но усложняет операции.
На практике для большинства API достаточно централизованного Redis с Lua-скриптами. Latency ~0.5ms на запрос — приемлемо для API с ответом в десятки миллисекунд.
Вывод
Rate limiting — обязательный компонент любого публичного API. Token bucket через Redis Lua — универсальное решение: атомарное, быстрое, поддерживает burst. Для простых случаев хватит nginx limit_req. Для микросервисов — Envoy или Kong с Redis-бэкендом. Главное — не забыть вернуть клиенту заголовки Retry-After и X-RateLimit-Remaining, чтобы он мог адаптироваться.