ГлавнаяБлогRate Limiting в Node.js: создаём Throttle-Box
Алгоритмы

Rate Limiting в Node.js: создаём Throttle-Box

Rate limiting в Node.js: реализация алгоритма token bucket без зависимостей. Узнайте, как создать гибкий лимитер для Express, Fastify, Koa и NestJS.

Al
Редакция Algolitalgolit.ru
8 мин чтения8 августа 2026 г.

Rate Limiting в Node.js: создаём Throttle-Box

Вы когда-нибудь сталкивались с тем, что ваш API начинает получать 10 000 запросов в минуту? Это может быть скрейпер, неправильно настроенный клиент или просто неожиданный успех. В любом случае ваша база данных горит, и вы жалеете, что не внедрили ограничение скорости с самого начала. В этой статье мы разберём, как создать собственный rate limiter на Node.js с нуля, без единой зависимости.

Проблема: почему существующие решения не подходят

Каждая библиотека для ограничения запросов, которую я рассматривал, имела свои недостатки:

  • Некоторые привязаны к одному фреймворку (только Express или только NestJS).
  • Другие тянут за собой 20+ транзитивных зависимостей.
  • Некоторые не поддерживают распределённое развёртывание.
  • Ядро логики часто настолько запутано, что его невозможно кастомизировать.

Я хотел создать инструмент, который:

  • Работает с Express, Fastify, Koa и NestJS.
  • Не имеет зависимостей (только встроенные модули Node.js).
  • Позволяет менять хранилище (в памяти для разработки, Redis для продакшена).
  • Настолько прост, что весь исходный код можно прочитать за один присест.

Что я построил: Throttle-Box

Throttle-Box использует алгоритм token bucket — один из самых интуитивно понятных подходов к ограничению запросов:

  • Бакет вмещает до capacity токенов (это ваш максимальный всплеск).
  • Каждый запрос потребляет один токен.
  • Токены пополняются непрерывно со скоростью refillRate в секунду (это ваша постоянная скорость).
  • Если бакет пуст → возвращается 429 Too Many Requests с заголовком Retry-After.

Например, capacity: 60, refillRate: 1 означает: можно мгновенно обработать 60 запросов, а затем — по 1 в секунду.

Что делает его особенным

Ноль зависимостей

Ничего. Ноль. Пакет работает на том, что уже есть в Node.js. Никакого риска для цепочки поставок, нечего аудитировать. При npm install вы получаете ровно один пакет.

Подключаемое хранилище

По умолчанию используется хранилище в памяти (идеально для однопроцессных приложений и тестов). Но интерфейс Store — это всего один метод: реализуйте consume(key, options), и вы можете использовать Redis, Postgres, DynamoDB или что угодно. Я даже экспортирую чистую функцию refillAndConsume, чтобы вы могли воспроизвести ту же математику в Redis Lua-скрипте для атомарного распределённого ограничения.

Ключи, маршруты, уровни

Вы можете выбирать ключи по IP, API-ключу, ID пользователя или любому другому параметру:

keyBy: (req) => req.headers['x-api-key'] ?? req.ip

Можно переопределить capacity и refillRate для каждого маршрута в момент запроса — более строгие лимиты для дорогих эндпоинтов, более высокие для платных тарифов:

dynamic: (req) => {
  if (req.path.startsWith('/admin')) return { capacity: 5, refillRate: 0.5 };
  if (req.user?.plan === 'pro') return { capacity: 1000, refillRate: 20 };
  return {};
}

Один пакет для четырёх фреймворков

// Express
app.use(expressRateLimit({ limiter }));

// Fastify
app.register(fastifyRateLimitPlugin({ limiter }));

// Koa
app.use(koaRateLimit({ limiter }));

// NestJS
@UseRateLimit({ capacity: 5, refillRate: 1 })
search() { ... }

Пакеты для фреймворков — это опциональные peer-зависимости: установите только тот, который используете. Ядро никогда не импортирует Express, Fastify, Koa или NestJS.

Заголовки, совместимые с RFC 9431

Каждый ответ получает стандартные заголовки RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset и RateLimit-Policy. Отклонённые запросы получают Retry-After. Ваши клиенты могут реализовать адаптивные повторные попытки без угадывания.

Плавная деградация

Если ваше Redis-хранилище выйдет из строя, Throttle-Box не остановит ваш API. Ошибки хранилища перехватываются, логируются через опциональный колбэк onError, и запрос пропускается. Rate limiting — это защита, а не единая точка отказа.

Цифры

  • 141 тест в 11 файлах.
  • 100% покрытие функций, 97.85% покрытие строк.
  • 31.5 kB опубликованный tarball (меньше, чем большинство README).
  • Протестировано на Node.js 20, 22, 24 и 26.
  • Двойная сборка ESM + CommonJS с полными типами TypeScript.

Как я это реализовал

Архитектура намеренно многослойная:

src/bucket.ts      → Чистый алгоритм (без I/O, без фреймворков, без Node-специфичных API)
src/store.ts       → Интерфейс Store + MemoryStore
src/limiter.ts     → Оркестратор, связывающий хранилище, извлечение ключа и логику пропуска
src/adapters/      → Клей для фреймворков (Express, Fastify, Koa)
src/nestjs/        → Guard, декоратор, динамический модуль для NestJS

Основные файлы (bucket.ts, store.ts, limiter.ts) не содержат импортов фреймворков. Это означает, что вы можете использовать логику ограничения в CLI-инструментах, WebSocket-серверах, фоновых задачах — где угодно, где работает Node.js.

Общая логика адаптеров находится в одной функции runMiddleware, которую вызывают все три HTTP-фреймворка. Добавление нового адаптера — это примерно 30 строк клея.

В ходе тестирования я исправил реальные баги — такие, которые проявляются только при написании тестов, имитирующих реальные пути отказа:

  • Синхронные колбэки dynamic(), выбрасывающие ошибки вне цепочки Promise (теперь обёрнуты в Promise.resolve().then()).
  • Сеттер ctx.body в Koa не срабатывал, потому что defaultReject находил метод send() раньше.

Что дальше

Пакет опубликован на GitHub с полной документацией, руководством по внесению вклада и CI-пайплайном, работающим на 4 версиях Node.js: github.com/hey-amanthakur/throttle-box. Скоро появится в npm как @hey-amanthakur/throttle-box.

Если вы создаёте API на Node.js и хотите ограничение запросов, которое не привязывает вас к фреймворку и не тянет дерево зависимостей — взгляните на этот проект. Обратная связь, вопросы и вклад приветствуются.

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

Не ждите, пока ваш API упадёт под нагрузкой. Начните с простого: установите rate limiter в своё приложение уже сегодня. Даже базовый лимитер на основе token bucket, работающий в памяти, защитит вас от случайных всплесков и скрейперов. А когда понадобится масштабирование — вы всегда сможете перейти на распределённое хранилище, не меняя логику.

Попробуйте Throttle-Box в своём проекте или реализуйте собственный rate limiter по описанной архитектуре. Главное — начните с малого и постепенно усложняйте.

#rate limiting#Node.js#token bucket#Throttle-Box
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →