Rate limiting в Node.js: реализация алгоритма token bucket без зависимостей. Узнайте, как создать гибкий лимитер для Express, Fastify, Koa и NestJS.
Вы когда-нибудь сталкивались с тем, что ваш API начинает получать 10 000 запросов в минуту? Это может быть скрейпер, неправильно настроенный клиент или просто неожиданный успех. В любом случае ваша база данных горит, и вы жалеете, что не внедрили ограничение скорости с самого начала. В этой статье мы разберём, как создать собственный rate limiter на Node.js с нуля, без единой зависимости.
Каждая библиотека для ограничения запросов, которую я рассматривал, имела свои недостатки:
Я хотел создать инструмент, который:
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.
Каждый ответ получает стандартные заголовки RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset и RateLimit-Policy. Отклонённые запросы получают Retry-After. Ваши клиенты могут реализовать адаптивные повторные попытки без угадывания.
Если ваше Redis-хранилище выйдет из строя, Throttle-Box не остановит ваш API. Ошибки хранилища перехватываются, логируются через опциональный колбэк onError, и запрос пропускается. Rate limiting — это защита, а не единая точка отказа.
Архитектура намеренно многослойная:
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 по описанной архитектуре. Главное — начните с малого и постепенно усложняйте.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →