ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

Rate limiting API на Redis и Lua: простой и быстрый лимитер для микросервисов

Разберёмся, как реализовать надёжный rate limiting для API на базе Redis и Lua. Сравним популярные алгоритмы лимитирования, спроектируем ключи и TTL, напишем компактные Lua‑скрипты и встроим такой limiter в микросервисную архитектуру.
Rate limiting API на Redis и Lua: простой и быстрый лимитер для микросервисов

Rate limiting для API — одна из тех вещей, о которых вспоминают в последний момент, когда фронт уже уехал в прод, а логов с ошибкой «429 Too Many Requests» ещё нет. Но как только ваш сервис начинает получать реальный трафик, особенно в микросервисной архитектуре, limiter оказывается критическим компонентом: он защищает бэкенды, базы, внешние интеграции и кошелёк в облаке.

В этой статье разберём, как сделать быстрый и предсказуемый rate limit на базе Redis и Lua: какие алгоритмы использовать, как проектировать ключи, писать скрипты и интегрировать limiter в микросервисы.

Зачем вообще нужен rate limit в API

Даже если вы не выпускаете публичный API, лимитирование запросов полезно почти в любом проекте:

  • Защита от «добросовестных» перегрузок — неудачный цикл на клиенте, сбой в мобильном приложении, кривой интеграционный скрипт партнёра.
  • Контроль стоимости — каждый запрос может триггерить тяжёлый SQL, обращение к внешнему API или очередь; ограничив RPS, вы стабилизируете нагрузку.
  • Чёткие ожидания для клиентов API — заранее понятные лимиты и ответы 429 вместо деградации по таймаутам.
  • Изоляция микросервисов — один «шуточный» сервис не должен выжигать ресурсы кластера.

Rate limit часто строят поверх Redis, потому что он быстрый, умеет атомарные операции и скрипты Lua, и уже есть в большинстве современных стеков, особенно там, где вы используете кэширование или храните сессии. Если вы только планируете инфраструктуру под API, удобно сразу закладывать Redis рядом с VDS-серверами для микросервисов, чтобы минимизировать задержки.

Почему Redis + Lua — хорошая связка для limiter

На первый взгляд может показаться, что достаточно инкремента счётчика по ключу и TTL, но на практике всплывают тонкости: гонки, несколько шагов логики, burst-окна, разные лимиты по ключам и т.п. Здесь Lua в Redis помогает собрать всю логику лимитера в один атомарный скрипт.

Комбинация Redis + Lua даёт:

  • Атомарность: вся логика rate limit — в одном Lua-скрипте, который Redis выполняет как единую операцию.
  • Минимум сетевых RTT: вместо нескольких команд (GET, INCR, EXPIRE, TTL) — один вызов EVAL или EVALSHA.
  • Гибкую бизнес-логику: разные лимиты для тарифов, IP, токенов, endpoint’ов — без усложнения кода сервиса.
  • Простую интеграцию: клиент Redis есть почти под любой язык; Lua-скрипты инкапсулируют сложность.

Идея: пусть у сервиса будет только один вызов «check_limit» в Redis. Всё остальное — детали реализации внутри Lua.

Схема сравнения алгоритмов rate limiting для API: fixed window, sliding window и token bucket в Redis

Основные алгоритмы rate limit: что выбрать под задачу

Перед тем как писать Lua, важно определиться с алгоритмом лимитирования. От него зависит поведение API под нагрузкой и предсказуемость ответов.

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

Самый простой вариант: для каждого интервала (например, минута) есть счётчик запросов. Преимущества:

  • Простая реализация: один ключ, один счётчик и TTL.
  • Понятные лимиты: «100 запросов в минуту» буквально означает максимум 100 в эту минуту.

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

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

Более точный алгоритм: смотрим запросы за последние N секунд относительно текущего времени, а не «с начала минуты». Обычно реализуется либо через sorted set, либо через time bucket’ы.

Плюсы:

  • Более ровное распределение запросов.
  • Нет резкого скачка на смене минуты.

Минусы — усложнение логики, больше данных в Redis (особенно если хранить каждый запрос и таймстамп).

Leaky bucket / Token bucket (ведро токенов)

Часто для API достаточно token bucket с допускаемым «burst» — коротким всплеском запросов. Идея: есть «ведро» с токенами. Токены добавляются с фиксированной скоростью (например, 10 в секунду) и не превышают максимум (например, 100). Каждый запрос «съедает» токен. Нет токенов — 429.

Преимущества token bucket:

  • Равномерный средний rate (например, 10 rps) с возможностью кратковременных всплесков (до 100 подряд).
  • Гибкое управление UX: пользователю можно позволить быстрее «выстрелить» несколько запросов, не ломая общую политику.

Недостаток — чуть более сложная реализация, нужно аккуратно считать время и наполнение ведра.

Практический выбор для микросервиса

В большинстве случаев для внешнего и B2B API хватает двух режимов:

  1. Per-API-key token bucket — для аутентифицированных запросов.
  2. Per-IP fixed window — грубая защита от анонимного шторма.

Оба варианта удобно реализовать в Redis + Lua с минимальными затратами. Похожие подходы хорошо сочетаются с кеширующими прослойками — об этом мы подробно писали в статье сравнение Memcached и Redis для кеша в PHP.

Дизайн ключей и TTL в Redis для rate limit

Как только вы определились с алгоритмом limiter’а, следующий важный шаг — проектирование ключей в Redis.

Как формировать ключи limiter’а

Основной принцип: ключ должен однозначно описывать «субъект лимита» и интервал. Обычно в ключ включают:

  • Пространство имён limiter’а (префикс), например: rl:.
  • Тип лимита: ip, user, apikey, endpoint.
  • Идентификатор: IP-адрес, user_id, api_key.
  • Окно или другую метку, если нужен time bucket (например, номер минуты).

Пример ключа для фиксированного окна «100 запросов в минуту по IP»:

rl:ip:203.0.113.10:202512011230

Здесь 202512011230 — усечённое время до минуты (год, месяц, день, час, минута) на стороне приложения.

TTL и очистка мусора

Для фиксированного окна TTL можно ставить немного больше длины окна, например, окно 60 секунд — TTL 70–90 секунд, чтобы не было пересечений и ключ самоуничтожался без лишних DEL.

Для token bucket ключ обычно один (без time bucket’ов), в нём храним, например, структуру:

  • оставшееся количество токенов;
  • последнее время обновления (timestamp).

TTL можно ставить чуть больше максимального «простоя» клиента. Если за это время он вообще не делает запросов, можно спокойно удалить ключ.

Простейший fixed window limiter на Redis + Lua

Начнём с самого простого — фиксированного окна. Алгоритм:

  1. Посчитать ключ для текущей минуты (или другого окна) на стороне приложения.
  2. В Lua-скрипте: увеличить счётчик, при необходимости поставить TTL.
  3. Вернуть клиенту, превышен лимит или нет, и текущее значение счётчика.

Пример Lua-скрипта для фиксированного окна (ограничение limit запросов за window секунд):

-- KEYS[1]  - ключ счётчика (например, rl:ip:203.0.113.10:202512011230)
-- ARGV[1] - limit (максимум запросов в окно)
-- ARGV[2] - window (длина окна в секундах)

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])

local current = redis.call("INCR", key)

if current == 1 then
  redis.call("EXPIRE", key, window)
end

if current > limit then
  return {0, current}
else
  return {1, current}
end

Сервис вызывает этот скрипт, передавая заранее вычисленный ключ и параметры лимита. Ответ — массив из двух элементов:

  • 1 или 0 — разрешён запрос или нет;
  • текущее значение счётчика.

На стороне приложения вы уже принимаете решение: отдавать 200 или 429, выставлять заголовки X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After и т.п.

Token bucket limiter на Redis + Lua

Теперь сделаем более интересный вариант — token bucket, который подходит для большинства API. Нам нужно хранить:

  • сколько токенов осталось;
  • когда мы последний раз обновляли ведро.

Это можно сделать разными способами: двумя ключами или одной строкой с сериализованными данными. На практике удобно хранить всё в одном HASH.

Схема данных для token bucket

Используем ключ вида rl:bucket:<id>, а внутри хэша поля:

  • tokens — текущие доступные токены;
  • ts — последний timestamp обновления.

Параметры ведра передаём в ARGV:

  • capacity — максимум токенов в ведре;
  • fill_rate — сколько токенов добавляем в секунду;
  • now — текущее время (секунды или миллисекунды);
  • ttl — TTL ключа (например, несколько минут или часов).

Lua-скрипт token bucket

-- KEYS[1]  - ключ ведра (например, rl:bucket:apikey:abc123)
-- ARGV[1]  - capacity (максимум токенов)
-- ARGV[2]  - fill_rate (токенов в секунду)
-- ARGV[3]  - now (текущее время в секундах)
-- ARGV[4]  - ttl (TTL ключа в секундах)

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

local bucket = redis.call("HMGET", key, "tokens", "ts")
local tokens = bucket[1]
local ts = bucket[2]

if tokens == false or tokens == nil then
  tokens = capacity
  ts = now
else
  tokens = tonumber(tokens)
  ts = tonumber(ts)
end

if now > ts then
  local delta = now - ts
  local filled = tokens + delta * fill_rate
  if filled > capacity then
    filled = capacity
  end
  tokens = filled
  ts = now
end

local allowed = 0

if tokens >= 1 then
  tokens = tokens - 1
  allowed = 1
end

redis.call("HMSET", key, "tokens", tokens, "ts", ts)
if ttl > 0 then
  redis.call("EXPIRE", key, ttl)
end

return {allowed, tokens}

Логика:

  1. Читаем текущие значения из Redis; если первый запрос — ведро заполнено полностью.
  2. Пересчитываем количество токенов, «доливая» их за прошедшее время.
  3. Если есть хотя бы один токен — разрешаем запрос и уменьшаем токены на 1.
  4. Сохраняем состояние ведра и выставляем TTL.

Ответ: {allowed, tokens_left}. Этого достаточно, чтобы на стороне сервиса принять решение и при желании добавить заголовки с остатками лимитов.

Интеграция limiter’а в микросервисную архитектуру

Один из ключевых вопросов: где должен жить limiter — в каждом микросервисе, в API gateway или в отдельном сервисе?

Вариант 1: лимитирование в API Gateway

Если у вас есть единый gateway (Nginx, Envoy, Kong и т.п.), логично реализовать rate limit в нём. Плюсы:

  • Единая точка контроля лимитов для всего API.
  • Снижение нагрузки на бэкенды — «лишние» запросы отсекаются на входе.
  • Проще менять политику лимитов, не трогая код микросервисов.

При этом сам gateway может использовать Redis + Lua (через, например, OpenResty) либо вызывать отдельный limiter-сервис, который использует Redis. Если вы уже используете Nginx как фронт, имеет смысл посмотреть и на заметку про балансировку HTTP/2 и gRPC с Lua — многие техники там пересекаются с темой лимитов.

Вариант 2: библиотека limiter’а в каждом микросервисе

Подход, когда каждая команда на своём языке добавляет поддержку rate limit через общий Lua-скрипт и конфиг, тоже рабочий, особенно если API не слишком публичный.

Плюсы:

  • Минимальная зависимость от инфраструктуры gateway.
  • Можно лимитировать не только HTTP-запросы, но и внутренние операции (например, обращения к БД или внешним API).

Минусы:

  • Сложнее поддерживать единые лимиты между командами.
  • Каждый язык и фреймворк — своя обёртка вокруг Redis.

Вариант 3: отдельный microservice-limiter

Ещё один вариант — вынести весь rate limiting в отдельный сервис, который принимает запросы типа «проверь лимит по таким параметрам» и отвечает JSON’ом. Внутри — Redis + Lua.

Преимущества:

  • Один источник правды по лимитам.
  • Можно использовать более сложную логику (комбинировать несколько ключей: IP + user + endpoint).

Недостатки — дополнительный hop (RTT) и нагрузка на сеть между сервисами и limiter’ом.

Архитектурная схема микросервисов с API gateway и лимитером запросов на Redis

Практические нюансы и анти-паттерны

Когда начинаешь внедрять rate limit в проде, возникают типичные вопросы и грабли.

Не делайте несколько команд вместо Lua-скрипта

Очень частая ошибка — реализовать limiter цепочкой команд:

GET key
INCR key
EXPIRE key window

Это:

  • неатомарно — под высокой нагрузкой появятся гонки;
  • даёт лишние RTT — увеличивает латентность API;
  • сложнее разнести на разные алгоритмы.

Lua-скрипт объединяет всё в одну атомарную операцию.

Измеряйте латентность Redis и учитывайте её в SLA API

Каждый запрос к limiter’у добавляет сетевой RTT до Redis. На локальном хосте это микросекунды–миллисекунды, в распределённой системе (отдельный кластер Redis) — уже десятки миллисекунд на хвосте.

Рекомендации:

  • Держите Redis как можно ближе к gateway или сервису (та же зона или датацентр).
  • Включите пулинг соединений в клиенте Redis.
  • Следите за P99 и P999 задержек Redis отдельно от задержки API.

Выбор ключа лимита: IP, токен, пользователь

Слишком грубые лимиты по IP для публичных API приводят к проблемам с NAT и корпоративными сетями: несколько разных пользователей за одним IP начнут получать 429.

Практический подход:

  • Для анонимных запросов — грубый лимит по IP (защититься от аномалий).
  • Для аутентифицированных клиентов — основной лимит по API-ключу или user_id.
  • При необходимости комбинируйте: «лимит по токену, но не больше N RPS суммарно по IP».

Экспорт лимитов наружу в заголовках

Если у вас внешний API, полезно отдавать клиентам информацию о лимитах в заголовках ответа, например:

  • X-RateLimit-Limit — общий лимит за окно;
  • X-RateLimit-Remaining — сколько ещё запросов можно сделать;
  • X-RateLimit-Reset — время сброса окна (timestamp или секунды до сброса);
  • Retry-After — сколько секунд ждать при ответе 429.

Это сильно снижает количество вопросов в поддержку и помогает клиентам API себя ограничивать до наступления 429.

Надёжность limiter’а: что будет, если Redis «упадёт»

Важно заранее решить, как ведёт себя ваш API, если Redis недоступен или сильно деградировал.

Fail-open или fail-closed?

Два базовых режима:

  • Fail-open: если Redis не отвечает, считаем, что лимитов нет, и пропускаем запросы.
  • Fail-closed: если Redis не отвечает, отклоняем запросы (например, 503).

Что выбрать, зависит от профиля сервиса.

  • Для публичного API с тяжёлым бэкендом часто разумнее fail-closed или частичный fail-closed: лучше временно защититься от лавины запросов, чем убить всю систему.
  • Для внутренних API, где rate limit скорее защита от ошибок клиента, можно выбрать fail-open, чтобы не блокировать бизнес-операции из-за падения Redis.

Репликация и Sentinel или Cluster

Если rate limit — критичный элемент для прод-API, стоит использовать отказоустойчивую схему Redis (Sentinel или Redis Cluster). При этом имейте в виду:

  • Lua-скрипты должны быть загружены на новый мастер или ноду; используйте EVALSHA и инициализацию скриптов при подключении.
  • Старайтесь не завязывать limiter на сильную консистентность между несколькими нодами кластера (для этого может потребоваться шардирование по ключу и sticky-ключи).

Минимальный чек-лист перед продом

Соберём в одном месте практический список, что проверить, прежде чем выпускать Redis + Lua limiter в бой.

  • Определены алгоритмы для разных типов клиентов: анонимные, авторизованные, админские.
  • Спроектированы ключи Redis с очевидными префиксами и понятными идентификаторами.
  • Lua-скрипты покрывают инициализацию значений при первом запросе, работу с time bucket’ами или токенами, корректный TTL.
  • API сервиса или gateway чётко трактует ответ скрипта и возвращает правильный HTTP-код (429, 200, 503) и заголовки.
  • Настроены метрики: количество 429, ошибки Redis, время выполнения Lua-скриптов.
  • Есть сценарий degraded-режима (fail-open или fail-closed), и он протестирован на staging.

Хорошо продуманный rate limit на Redis + Lua — это не только защита от DDoS-подобной нагрузки, но и важный элемент экосистемы API. Он помогает держать микросервисы под контролем, избегать неожиданных провалов производительности и обеспечивать предсказуемый опыт для клиентов.

Если у вас уже есть Redis в стеке, добавить к нему Lua-лимитер — логичный шаг: минимальные изменения в коде сервисов, понятные метрики и возможность со временем развивать более сложные политики лимитирования, не ломая существующих клиентов. А если вы только строите инфраструктуру под API, подумайте, на каких серверах всё это будет крутиться: надёжный виртуальный хостинг под веб-приложения или управляемый кластер на VDS сильно упрощают жизнь при масштабировании лимитеров и самого API.

Поделиться статьей

Вам будет интересно

Staging для WordPress на VDS: безопасные обновления без даунтайма OpenAI Статья написана AI (GPT 5)

Staging для WordPress на VDS: безопасные обновления без даунтайма

Staging-окружение для WordPress на VDS позволяет обновлять плагины, тему и ядро без риска для боевого сайта и без даунтайма. Разбе ...
CI/CD артефакты в S3 Object Storage: как раздавать через Nginx с правильным Cache-Control OpenAI Статья написана AI (GPT 5)

CI/CD артефакты в S3 Object Storage: как раздавать через Nginx с правильным Cache-Control

Артефакты CI/CD всё чаще выносят в S3‑совместимый Object Storage: билды, фронтенд‑статику, отчеты, архивы. Чтобы избежать проблем ...
PostgreSQL read-only реплики на VDS: быстрые отчёты без нагрузки на прод OpenAI Статья написана AI (GPT 5)

PostgreSQL read-only реплики на VDS: быстрые отчёты без нагрузки на прод

Разберём, как настроить read-only реплику PostgreSQL на VDS через streaming replication, чтобы вынести отчёты, аналитику и тяжёлые ...