Сравнение библиотек ограничения частоты запросов Node.js для публичных API
Сравните Node.js-библиотеки для ограничения частоты запросов в публичных API — от простого middleware до вариантов с Redis, которые стабильно работают за прокси.

Почему один клиент может перегрузить публичный API
Публичный API делит один и тот же запас CPU, времени базы данных, места в кеше и пропускной способности сети между всеми, кто к нему обращается. Это кажется очевидным, но сбойный сценарий легко пропустить: один шумный клиент может быстро съесть этот запас, даже если общий трафик на панели выглядит «нормально».
Ущерб часто начинается на одном дорогом маршруте. Простой endpoint GET /search или POST /export может обращаться к базе данных, собирать ответ и держать worker занятым секунду или две. Если один клиент отправит 30 запросов одновременно, этот один маршрут может вытеснить более лёгкие запросы всех остальных. Пользователи, которые ничего не нарушали, начинают видеть медленные ответы или таймауты.
К этому снова и снова приводят несколько типичных сценариев:
- Резкий всплеск трафика после багнутого релиза, cron-задачи или множества открытых вкладок браузера
- Циклы повторных попыток, которые продолжают срабатывать после таймаутов, ошибок 500 или разрывов соединения
- Простое бот-злоупотребление вроде парсинга, спама регистраций или перебора на публичных endpoints
Короткие всплески и постоянное злоупотребление вредят по-разному. Краткий пик забивает очереди, съедает слоты соединений и может пройти за минуту, но всё равно вызывает волну ошибок. Постоянное злоупотребление тише и часто опаснее. Бот, который отправляет ровно столько трафика, чтобы не бросаться в глаза, может держать ваш API занятым весь день и повышать расходы, не вызывая одной очевидной аварии.
Аутентификация сама по себе этого не решает. Авторизованный пользователь всё равно может завалить маршрут запросами. API-ключи могут утечь. Партнёрская интеграция может отправить плохую логику повторов. Некоторые атакующие используют много аккаунтов вместо одного. Если сервер принимает запрос и начинает работу, перегрузка возможна и для анонимного клиента, и для вошедшего в систему.
Поэтому лимиты по частоте запросов должны соответствовать реальной мощности, а не просто блокировать очевидные атаки. Команды, которые работают с экономной инфраструктурой, быстро это понимают. Oleg Sotnikov часто пишет о сокращении потерь на уровне архитектуры, и здесь тот же принцип работает так же: сначала защищайте общие ресурсы, особенно те маршруты, которые делают больше всего работы на каждый запрос.
Без этой защиты один клиент не обязан «ронять» весь API, чтобы нанести реальный ущерб. Ему достаточно долго удерживать самый загруженный путь занятым, чтобы платить за это пришлось всем остальным.
Что сравнивать в Node.js rate limit tools
Два пакета могут оба называться «rate limiter», но при реальном трафике вести себя совсем по-разному. Для публичного API первый вопрос простой: что именно считает инструмент и где он хранит этот счёт?
Некоторые инструменты считают каждый запрос по IP. Это хорошо для открытых endpoints, но может наказывать общие сети в офисах, школах или у мобильных операторов. Другие умеют считать по API key, user ID или даже по маршруту, и это даёт больше контроля. Для login-маршрута, search-маршрута и платного data endpoint часто нужны разные правила.
Не менее важен и сам тип правила. Фиксированное окно легко объяснить, но оно может разрешить всплеск прямо перед и сразу после сброса окна. Скользящее окно сглаживает это, хотя часто требует больше хранилища или логики. Token bucket — хороший компромисс, если вы хотите разрешать короткие всплески, но останавливать постоянное злоупотребление.
Хранилище определяет, останутся ли лимиты точными, когда приложение вырастет. Счётчики в памяти подходят для одного небольшого сервера или локального тестирования. Но они перестают работать, как только у вас несколько Node.js-инстансов, контейнеров или регионов. Для общих лимитов обычно выбирают Redis, потому что каждый экземпляр приложения может читать и обновлять один и тот же счётчик.
Когда сравниваете Node.js rate limiting libraries, проверьте такие вещи:
- какой идентификатор они умеют отслеживать: IP, API key, user ID, session или route
- какой алгоритм они используют и можно ли настроить поведение при всплесках
- остаётся ли хранилище корректным в кластерах и после перезапусков
- как они отправляют ответы 429, заголовки rate limit и время повторной попытки
- как они читают адреса клиентов за прокси и с IPv6
Именно последний пункт часто создаёт больше всего проблем. Если вы работаете за Cloudflare, nginx или load balancer, приложение может видеть только IP прокси, если не настроить доверенные forwarded headers правильно. IPv6 тоже может удивлять: один пользователь может выглядеть как множество близких адресов. Пакет, который хорошо умеет работать с throttling с учётом proxy, сэкономит вам и ложные блокировки, и пропущенные злоупотребления.
Middleware для Express и Fastify
Если ваш API уже работает на Express или Fastify, framework middleware часто оказывается самым быстрым способом добавить защиту от злоупотреблений. Его можно подключить на уровне приложения для общих лимитов, а затем сделать более строгие правила только для маршрутов, которые привлекают спам, например поиска, регистрации или создания токенов.
В Express обычно начинают с express-rate-limit. Он простой, читаемый и хорошо подходит для жёстких ограничений вроде «100 запросов в минуту на IP». Для небольшого публичного API на одном сервере этого может быть достаточно, чтобы остановить одного нетерпеливого клиента, который съедает мощность, предназначенную для всех остальных.
express-slow-down решает другую задачу. Вместо мгновенной блокировки он добавляет задержку после того, как клиент пересёк порог. Это хорошо работает на шумных маршрутах, где вы хотите сделать злоупотребление дорогим ещё до ответа 429. Часто используют такой подход: сначала мягкое замедление, потом жёсткая блокировка для тех, кто продолжает давить.
Практический шаблон для Express выглядит так:
- использовать
express-rate-limitглобально с умеренным лимитом - добавить
express-slow-downна маршруты, которые атакуют чаще всего - поставить более строгий жёсткий лимит на те же шумные маршруты
Такой набор лучше воспринимается обычными пользователями. Тот, кто несколько раз обновил страницу, может увидеть лишь небольшую задержку, а скрипт, который продолжает строчить запросы, будет остановлен.
Если вы используете Fastify, подойдёт @fastify/rate-limit. Он хорошо вписывается в plugin model Fastify, поэтому лимиты легко применять и к отдельным маршрутам, и ко всему приложению. Это делает код аккуратнее, особенно когда разным endpoints нужны разные правила.
Framework middleware удобно тем, что он живёт рядом с маршрутами. Вам не нужен отдельный gateway, чтобы протестировать простую политику, и команде обычно хватает одного файла, чтобы понять настройку. Это особенно важно, когда после запуска реального трафика нужно быстро подправить лимиты.
Но есть и ловушка: стандартное хранилище обычно работает в памяти. А это ломается, как только у вас больше одного экземпляра приложения. Каждый replica ведёт свои собственные счётчики, поэтому клиент может распределять трафик между нодами и обходить задуманный лимит. Перезапуск тоже обнуляет счётчики, давая злоумышленникам новый старт.
Поэтому in-memory лимиты подходят для локального тестирования, низкорисковых внутренних инструментов или одного совсем маленького деплоя. Для публичного API с репликами нужны общие счётчики. Redis или другое центральное хранилище превращают отдельные экземпляры приложения в один согласованный rate limiter.
Пакеты для общих счётчиков и Redis
Если ваш API работает на нескольких Node.js-инстансах, лимиты в памяти быстро ломаются. Клиент может отправить запросы на instance A, затем на B, затем на C — и каждый сервер будет думать, что клиент всё ещё в пределах лимита. Общие счётчики решают эту проблему. Каждый экземпляр видит один и тот же счёт, поэтому лимит остаётся одинаковым.
rate-limiter-flexible — частый выбор, когда нужен такой общий обзор. Он хорошо работает с Redis и даёт больше контроля, чем базовые middleware stores. Этот контроль особенно важен, когда злоупотребление идёт неравномерно: один IP резко вспыхнул, успокоился, а через пять минут снова вспыхнул.
Простая схема с Redis помогает сразу с несколькими вещами:
- один лимит на все экземпляры приложения
- блокировки, которые переживают перезапуск приложения
- свои штрафы для повторных нарушителей
- более аккуратная обработка коротких всплесков
Разница между пакетами обычно проявляется в том, как они наказывают и как восстанавливают доступ. В rate-limiter-flexible можно задать обычный лимит, а затем добавить длительность блокировки, если клиент перешёл черту. Можно также начислять штрафные очки после плохого поведения — это полезно для неудачных попыток входа или паттернов парсинга. Некоторые более лёгкие Express middleware тоже умеют считать запросы в Redis, но часто ограничиваются только «слишком много запросов» и не дают большого пространства для адаптивных правил.
Работа со всплесками важнее, чем многие команды ожидают. Публичные API часто получают небольшие законные пики из-за переподключений мобильных устройств, повторных попыток или batch-задач. Жёсткий лимит может блокировать нормальных пользователей. Более удачная настройка сначала разрешает короткий всплеск, а затем замедляет или блокирует длительное превышение. Общие счётчики помогают, потому что каждый сервер видит один и тот же всплеск, а не считает его за три маленьких.
Redis стоит дополнительной настройки, когда лимиты должны оставаться точными под нагрузкой. Обычно это означает несколько экземпляров приложения, длинные периоды блокировки или любой случай, когда перезапуск не должен обнулять всё. Если у вас один небольшой внутренний сервис, памяти часто достаточно. Если у вас публичный API за load balancer, простота настройки не должна выигрывать у стабильности enforcement.
Вот tradeoff, который многие команды упускают: простой вариант экономит 20 минут в первый день, а потом каждый день тихо теряет мощность.
Как сделать лимиты надёжными за прокси и load balancer
Многие Node.js rate limiting libraries отлично выглядят в локальном тесте, а потом ломаются в момент, когда вы ставите API за Cloudflare, nginx или load balancer. Обычно ошибка простая: приложение считает IP прокси, а не реального клиента. Тогда один загруженный офис, мобильный оператор или gateway может заставить случайных пользователей упереться в один и тот же лимит.
Доверяйте правильным заголовкам
Приложение должно читать адрес клиента из доверенных proxy headers, но только от тех прокси, которые вы контролируете. Если доверять любому forwarded header из открытого интернета, любой человек сможет подменить свой IP и обойти лимит.
На практике сначала задайте правила доверия к прокси, а затем сделайте так, чтобы limiter использовал уже вычисленный client IP. Проверьте это на реальном пути запроса, а не только на localhost. Запрос, который проходит через Cloudflare и nginx, может выглядеть совсем не так, как запрос, который приходит в Express напрямую.
Используйте идентификатор, который соответствует реальному трафику
Один только IP часто слишком грубый инструмент. Компания может проводить сотни пользователей через один исходящий gateway. Мобильная сеть может держать многих людей за одним адресом. Если ваш API использует ключи или токены, соединяйте их с IP там, где это имеет смысл. Так лимит будет честнее и злоупотреблять станет сложнее.
Для многих публичных API хорошо работает простое правило:
- используйте API key, когда клиент авторизован
- переходите на IP для анонимного трафика
- смешивайте API key и IP для чувствительных маршрутов вроде входа или поиска
- логируйте итоговый идентификатор для каждого события блокировки
Последний пункт важнее, чем кажется. Когда в поддержку приходит жалоба, нужно точно видеть, что именно считал limiter. Логируйте выбранный идентификатор, маршрут и причину блокировки. Иначе ложные срабатывания превращаются в догадки.
Если у вас больше одного Node.js-инстанса, храните счётчики вне приложения. Лимиты в памяти быстро ломаются, потому что каждая нода видит только часть трафика. Общий store, например Redis, даёт всем серверам один и тот же счёт, и лимит остаётся одинаковым между replica и регионами.
Полезна быстрая проверка: отправьте десять запросов через весь proxy chain, а затем убедитесь, что в логах на каждой ноде виден один и тот же client identity. Если это не так, исправьте именно это, прежде чем настраивать числа.
Как выбрать пакет шаг за шагом
Выбирайте пакет, сопоставляя его с тем, как ваш API реально работает. Когда сравниваете Node.js rate limiting libraries, на минуту забудьте о списках функций и посмотрите на путь трафика. Express-приложение на одной VM может обойтись простым in-memory limiter. API, распределённый по нескольким Node.js-инстансам, — нет.
Если запросы могут попадать на разные серверы, каждому серверу нужны одни и те же счётчики. Обычно это означает Redis. Здесь пакеты делятся на две группы: простые middleware, которые хорошо работают на одном экземпляре, и инструменты, рассчитанные на общие счётчики между многими экземплярами.
Не менее важен и идентификатор клиента. IP-адрес удобен, но он часто ломается в офисах, мобильных сетях и любой схеме за Cloudflare, nginx или другим proxy. Если ваш API использует логин или API keys, используйте их, когда можете. Для смешанного трафика часто лучше работает комбинация: API key для подписанных запросов, IP для анонимных.
Не давайте всем маршрутам один и тот же бюджет. Health check или небольшой read endpoint может выдержать куда больше трафика, чем маршрут, который запускает тяжёлый поиск, создаёт отчёт или вызывает LLM. Если один endpoint стоит в 50 раз дороже, ограничивайте его отдельно. Так дешёвый трафик будет идти дальше, а вы защитите места, которые сжигают CPU, время базы данных или деньги.
Затем решите, насколько жёстко должен срабатывать limiter. Некоторые пакеты только блокируют. Другие могут сначала замедлить клиента, а потом заблокировать. Для публичных API это часто лучший вариант. Небольшие всплески проходят, повторное злоупотребление замедляется, а очевидные атаки останавливаются.
Перед запуском проверьте работу с реальными proxy headers, а не только на localhost. Отправляйте запросы через тот же load balancer, CDN или reverse proxy, который используете в production, и убедитесь, что приложение видит правильный client address. Если этот кусок настроен неверно, даже хорошая защита от злоупотреблений будет блокировать невиновных пользователей или пропускать шумных.
Простое правило работает хорошо: один сервер — памяти может хватить, больше одного сервера — нужен Redis, а любой публичный endpoint за прокси требует аккуратной проверки идентификатора, прежде чем ему можно доверять.
Простой пример публичного API
Представим публичный search API с двумя тарифами. Бесплатные пользователи могут выполнять обычные поисковые запросы. Платные пользователи тоже могут искать, а ещё им доступны export jobs, которые собирают CSV-файлы в фоне.
Обычно всё работает нормально. Но затем один клиент выпускает плохой retry loop после ошибки таймаута. Их приложение повторяет один и тот же поисковый запрос снова и снова, иногда по 20 раз на один клик. Задержка поиска растёт, workers остаются занятыми, и другие клиенты начинают это чувствовать.
Хорошая схема лимитов по-разному относится к поиску и экспортам. Поиск частый и обычно лёгкий, поэтому для бесплатного трафика не нужен мгновенный блок. Лучше работает мягкая задержка. После небольшого лимита на всплеск API всё ещё отвечает, но перед ответом ждёт 200–500 мс. Настоящие пользователи продолжают искать. Скрипт-парсер или сломанный клиент быстро замедляется.
Export jobs требуют более жёсткого правила. Каждый job может обращаться к базе данных, ставить задачи в очередь и держать CPU занятым минуты. Если бесплатным пользователям разрешить неограниченное количество экспортов, один скрипт сможет съесть много общей мощности. Поставьте строгий лимит на создание jobs и отклоняйте лишние запросы понятным ответом 429.
Простой набор правил может выглядеть так:
- Бесплатный поиск: 60 запросов в минуту на API key, затем короткая задержка
- Анонимный поиск: 30 запросов в минуту на IP, затем такая же задержка
- Бесплатные export jobs: 3 в час, затем отклонять новые jobs до сброса окна
- Платные export jobs: гораздо более высокий лимит, но всё равно жёсткий
Сначала считайте по API key. Так вы не смешаете в одну корзину разных пользователей за одним офисным или мобильным IP. Когда трафик анонимный, переходите на IP, чтобы у API всё ещё было базовое правило.
Храните счётчики в Redis, а не в локальной памяти. Если API работает на трёх Node.js-инстансах, каждому нужно видеть одни и те же счётчики запросов и cooldowns. Redis делает это возможным, поэтому клиент, заблокированный на одном экземпляре, не проскочит через другой.
Многие Node.js rate limiting libraries хорошо подходят для такого сценария. Хорошие лимиты сохраняют API удобным для всех, даже когда один клиент ведёт себя плохо.
Ошибки, которые вызывают ложные блокировки или пропускают злоупотребления
Rate limiter может сломаться двумя способами. Он может блокировать нормальных пользователей, а может пропускать вредный трафик. Многие команды случайно делают и то и другое, даже с приличными Node.js rate limiting libraries.
Одна из самых частых ошибок — доверять любому forwarded header. Если приложение принимает X-Forwarded-For из открытого интернета, клиент может подменить IP и обойти лимиты, каждый раз используя фальшивый адрес. Доверяйте forwarded headers только от своего proxy или load balancer. Если эта цепочка доверия сломана, limiter слеп.
Одна общая глобальная граница тоже создаёт проблемы. Login, search и тяжёлые export-отчёты стоят по-разному. Если дать всем маршрутам один общий бюджет, обычный пользователь может упереться в лимит просто из-за пары дорогих отчётов. В то же время атакующий может обходить настоящие точки давления, потому что правила слишком широкие. Ставьте более строгие лимиты на дорогие или чувствительные маршруты и более мягкие — на дешёвые чтения.
Счётчики в памяти на локальном тесте выглядят нормально, а в production ломаются. Если у вас несколько pod’ов или серверов, каждый экземпляр видит только часть трафика. Один клиент может прыгать между экземплярами и получать реальный лимит намного выше, чем вы планировали. Общий store, чаще всего Redis, исправляет это, потому что каждый экземпляр читает и пишет одни и те же счётчики.
Отслеживание только по IP тоже может наказать не тех людей. Целый офис, школа или мобильный оператор могут выглядеть как один IP. Если один человек ведёт себя плохо, заблокировать могут всех за этим адресом. Для публичных API это плохой опыт. По возможности отслеживайте больше, чем IP: например, API key, user ID, session или комбинированный fingerprint.
Важно и то, как вы обрабатываете ошибки. Простой 500 скрывает, что произошло, и заставляет клиентов повторять запросы ещё агрессивнее. Возвращайте 429 при превышении лимита и говорите клиенту, как долго ждать. Даже простое значение ожидания или заголовок Retry-After помогает нормальным клиентам сделать паузу вместо того, чтобы давить сильнее.
Более безопасная схема обычно следует нескольким правилам:
- доверяйте proxy headers только известной инфраструктуре
- используйте отдельные лимиты для входа, поиска, записей и тяжёлых jobs
- храните счётчики в общем backend-хранилище
- идентифицируйте клиентов по API keys или user IDs, когда это возможно
- возвращайте понятные ответы 429 с временем повторной попытки
Если один и тот же офис блокируется после одного всплеска, а атакующий продолжает ротировать фальшивые IP без сопротивления, limiter не строгий. Он просто направлен не туда.
Быстрая проверка перед запуском
Rate limit, который выглядит нормально в staging, всё равно может сломаться в production по простым причинам. Один маршрут получает слишком строгий лимит, хотя делает дешёвое чтение. Другой остаётся слишком открытым, хотя запускает тяжёлый запрос к базе данных или отправку email. Сопоставляйте каждое правило с реальной стоимостью маршрута, а не с приблизительной догадкой.
Публичный endpoint /search, например, может нуждаться в более жёстком лимите, чем /status, даже если оба используют GET. Login, reset пароля, export и webhook endpoints обычно тоже нуждаются в собственных правилах. Один общий лимит на весь API легко выпустить, но он часто блокирует нормальных пользователей и пропускает дорогие злоупотребления.
Перед тем как включить
Проверьте следующий момент — client identifier. Если приложение стоит за Cloudflare, nginx или load balancer, убедитесь, что оно читает правильный IP или token и не rate limit’ит сам proxy. В Express это обычно означает корректную настройку доверия к proxy и проверку с реальными forwarded headers.
Затем проверьте, где живут счётчики. Если лимиты сбрасываются при каждом деплое или отличаются между экземплярами приложения, защита в основном фиктивная. Для публичных API счётчики обычно должны жить в Redis или другом общем store, чтобы каждый сервер видел одни и те же числа.
Короткий чек-лист перед запуском помогает:
- Дайте каждому дорогому маршруту своё правило.
- Проверьте, что приложение записывает реальный адрес клиента.
- Храните счётчики вне памяти процесса.
- Логируйте имя правила, идентификатор и каждый результат 429.
- Возвращайте понятное время повторной попытки, обычно через заголовок
Retry-After.
Логи важнее, чем кажется многим командам. Когда клиент говорит: «ваш API нас заблокировал», вам нужно видеть, какое правило сработало, какой идентификатор совпал и как часто это происходило. Без этих данных настройка превращается в угадывание.
Понятные ответы помогают хорошим клиентам быстро восстановиться. Обычный body для 429 с временем ожидания, плюс стабильные заголовки, позволяют SDK и скриптам сделать паузу вместо того, чтобы бить ещё сильнее. Эта маленькая деталь одновременно экономит время поддержки и защищает общую мощность.
Следующие шаги для вашей команды
Начните с малого. Выберите один или два маршрута, которые могут съедать больше всего общей мощности, например login, search или публичный endpoint export. С одного шумного пути вы узнаете больше, чем от общих правил для всех endpoints сразу.
Обычно лучше работает такой простой запуск:
- Добавьте один лимит для коротких всплесков и один для постоянного трафика.
- Храните счётчики в Redis, если у вас больше одного экземпляра приложения.
- Логируйте идентификатор клиента, который вы блокируете — IP, API key, user ID или их сочетание.
- Возвращайте понятные ответы 429 с временем повторной попытки.
- Сначала держите пороги немного мягче.
Потом наблюдайте данные целую неделю. Ложные срабатывания быстро заметны, когда офисные сети, мобильные операторы и общие прокси выглядят как один клиент. Просматривайте каждый всплеск 429, проверяйте, какие идентификаторы были заблокированы, и сравнивайте это с нормальным трафиком, прежде чем что-то ужесточать.
Заранее настройте алерты. Резкий рост 429 может означать, что правила слишком строгие, но может и значить, что кто-то нашёл дорогой маршрут и начал его атаковать. Отслеживайте и число заблокированных запросов, и число заблокированных идентификаторов. Эти две метрики рассказывают историю лучше, чем один только объём запросов.
Если вы всё ещё сравниваете Node.js rate limiting libraries, держите в голове весь стек. Лимиты, хранилище, прокси и порядок запуска должны работать вместе. Когда это начинает путаться, короткая консультация с Oleg Sotnikov может сэкономить много проб и ошибок. Его работа как Fractional CTO включает помощь командам в выборе lean-инфраструктуры, лимитов, безопасных для proxy, и планов запуска, которые защищают публичные API, не блокируя обычных пользователей.