Как я создал Rate Limiter на основе скользящего окна в Redis

Наш видео-API раньше падал каждый вечер в 20:00 UTC.

Это был не реальный трафик. Несколько скраперов нашли наш эндпоинт с трендами и начали засыпать его запросами. Запросы к нашей базе данных накапливались в очереди. Реальные пользователи видели индикаторы загрузки вместо видео.

Нам нужно было ограничение частоты запросов (rate limiting). Большинство простых методов только усугубляли проблему.

Проблема счетчиков с фиксированным окном (Fixed Window):

Большинство начинают с фиксированных окон. Вы считаете запросы в определенном временном блоке. Если ваш лимит — 100 запросов в минуту, пользователь может отправить 100 запросов в 11:59:59 и еще 100 в 12:00:00. Это 200 запросов за одну секунду. Скраперы используют эти границы, чтобы обходить ваши лимиты.

Решение: Скользящее окно (Sliding Window).

Скользящее окно считает запросы за последние N секунд относительно текущего момента. Здесь нет границ, которые можно использовать для обхода.

Мы используем два разных метода в зависимости от наших потребностей:

1. Лог скользящего окна (Sliding Window Log — для точности)

Мы используем Redis Sorted Sets (ZSET) для аутентифицированных пользователей. Мы сохраняем временную метку (timestamp) для каждого запроса.

  • Мы используем ZREMRANGEBYSCORE, чтобы удалять старые записи.
  • Мы используем ZCARD, чтобы считать текущие запросы.
  • Мы используем ZADD, чтобы записывать новые.

Чтобы предотвратить состояние гонки (race conditions), мы запускаем эту логику в одном Lua-скрипте. Это делает операцию атомарной. Один скрипт выполняет всё за один проход к Redis. Это предотвращает ситуацию, когда два запроса одновременно считают, что они не превышают лимит.

2. Счетчик скользящего окна (Sliding Window Counter — для масштабируемости)

Для анонимного трафика у нас миллионы ключей. Мы не можем хранить каждую временную метку. Вместо этого мы используем счетчик, который взвешивает текущее и предыдущее окна. Это потребляет гораздо меньше памяти и работает очень эффективно.

Ключевые уроки из продакшена:

  • Используйте Lua-скрипты. Это гарантирует, что проверка и запись происходят как одно единое действие.
  • Выпускайте стандартные заголовки. Всегда отправляйте X-RateLimit-Remaining и Retry-After. Это помогает корректным клиентам автоматически снижать нагрузку.
  • Принцип "fail open". Если Redis упадет, разрешайте запрос. Перегруженный API лучше, чем полностью неработающий.
  • Правильно идентифицируйте клиентов. Используйте API-ключи для пользователей и хешированные IP для гостей.
  • Соответствуйте лимиты эндпоинтам. Тяжелому эндпоинту поиска нужны более строгие лимиты, чем простому эндпоинту со списком.

Результат был мгновенным. Вечерние скачки исчезли. Производительность нашей базы данных вернулась в норму.

Source: https://dev.to/ahmet_gedik778845/how-i-built-a-sliding-window-rate-limiter-for-our-video-api-in-redis-4k1c