Rate limiting в Nginx: limit_req, burst и защита API от перегрузки
Как работает алгоритм «дырявого ведра», что на самом деле делают burst, nodelay и delay, как отдавать 429, не задеть свой мониторинг и проверить лимиты до того, как они срежут живых пользователей.

Коротко: rate limiting в Nginx настраивается двумя директивами. limit_req_zone в блоке http задаёт ключ (обычно IP клиента), размер зоны в памяти и скорость, а limit_req в location включает лимит с запасом burst. Для API добавьте limit_req_status 429, вынесите мониторинг в белый список, первую неделю держите лимит в limit_req_dry_run и только потом включайте отказы.
Один забытый скрипт, который опрашивает /api/orders в цикле без паузы, способен положить бэкенд не хуже целенаправленной атаки. То же делают парсеры цен, перебор паролей на /login, мобильное приложение с ошибкой в ретраях. Nginx стоит перед приложением, видит каждый запрос и умеет отсечь лишнее до того, как запрос дойдёт до PHP, Python или базы.
У этого инструмента есть граница. Nginx считает запросы по IP или по заголовку, но не знает, какой у клиента тариф, сколько вызовов ему положено в месяц и какие эндпоинты дорогие. Такие квоты — часть логики самого сервиса, и закладывают их на этапе проектирования. Если вы отдаёте эту работу команде, которая проектирует и разрабатывает API, спросите заранее, где будут жить лимиты: на входе в Nginx, в приложении или в обоих местах. Ниже — как сделать свою часть на уровне веб-сервера правильно: с командами, расчётами и типичными ошибками.
Как работает rate limiting в Nginx?
Nginx использует алгоритм leaky bucket — «дырявое ведро». Запросы клиента наливаются в ведро, а вытекают из него с постоянной скоростью, заданной в rate. Если ведро переполнено, лишний запрос отклоняется.
Скорость 10r/s не означает «десять запросов в любой момент секунды». Она означает «один запрос каждые 100 мс». Если клиент пришлёт два запроса с разницей в 20 мс, без параметра burst второй получит отказ, хотя за секунду их было всего два. Это главный сюрприз для новичков: лимит работает с миллисекундной точностью, а не как счётчик за секунду.
Для каждого значения ключа Nginx хранит в разделяемой памяти небольшое состояние: сколько «воды» в ведре и когда был последний запрос. Память общая для всех воркер-процессов одного экземпляра Nginx, поэтому лимит не зависит от того, какой воркер принял соединение.
В Nginx есть два модуля ограничений, и их часто путают:
| Модуль | Что ограничивает | Ключевые директивы | Против чего |
|---|---|---|---|
ngx_http_limit_req_module | частоту запросов во времени | limit_req_zone, limit_req | флуд, перебор паролей, парсеры, зацикленные клиенты |
ngx_http_limit_conn_module | число одновременных соединений | limit_conn_zone, limit_conn | медленные загрузки, удержание соединений, массовое скачивание файлов |
Оба модуля входят в стандартную сборку Nginx из пакетов Debian, Ubuntu, RHEL и официального репозитория nginx.org — ничего дополнительно ставить не нужно.
Как настроить limit_req_zone и limit_req?
Базовая настройка состоит из двух шагов: объявить зону в http и применить её в нужном location.
Шаг 1. Объявить зону
# /etc/nginx/conf.d/ratelimit.conf (подключается внутри http {})
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_status 429;
limit_req_log_level warn;
Разберём строку по частям:
$binary_remote_addr— ключ, IP клиента в двоичном виде: 4 байта для IPv4 и 16 для IPv6. Строковый$remote_addrзанимает до 39 байт и съедает память зря.zone=api:10m— имя зоны и её размер.rate=10r/s— скорость. Для редких операций используют запросы в минуту:rate=5r/m.
Шаг 2. Применить лимит
server {
listen 443 ssl;
server_name api.example.com;
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://127.0.0.1:8080;
}
}
Проверьте синтаксис и перечитайте конфигурацию без разрыва соединений:
sudo nginx -t && sudo systemctl reload nginx
Сколько памяти выделить зоне?
По документации Nginx на 64-битной платформе состояние для ключа $binary_remote_addr занимает 128 байт, и в 1 МБ помещается около 8 тысяч состояний. Во многих руководствах пишут про 16 тысяч — это цифра для 32-битных систем.
| Размер зоны | Примерно уникальных IP (64 бит) | Подходит для |
|---|---|---|
| 1m | 8 000 | внутренний сервис, админка |
| 10m | 80 000 | обычный сайт или API |
| 50m | 400 000 | крупный публичный API |
Когда зона заполняется, Nginx удаляет самые давние состояния. Если освободить место не удалось, запрос получает ошибку — так что маленькая зона на большом трафике сама становится источником отказов.
Что делают burst, nodelay и delay?
Без burst лимит слишком строгий для реальных браузеров. Открытие одной страницы — это пачка из 10–30 запросов к API и статике за долю секунды. burst задаёт глубину ведра: сколько запросов сверх скорости можно принять, прежде чем начать отказывать.
burst без nodelay: очередь
limit_req zone=api burst=20; — лишние запросы встают в очередь и отдаются бэкенду строго по графику, один в 100 мс. Двадцатый запрос пачки будет ждать около двух секунд. Отказы начнутся, только когда в очереди окажется больше 20 запросов. Для API это обычно плохо: клиент видит необъяснимые задержки.
burst с nodelay: мгновенно, но с учётом
limit_req zone=api burst=20 nodelay; — все 20 лишних запросов проходят сразу, без задержки, но занимают места в ведре. Места освобождаются со скоростью rate, то есть по одному в 100 мс. Если клиент продолжит слать запросы быстрее, он получит 429. Для API и веб-приложений это самый практичный вариант.
delay: гибрид
С версии 1.15.7 есть параметр delay=N: первые N лишних запросов проходят мгновенно, следующие до burst задерживаются, а сверх burst отклоняются.
limit_req zone=api burst=20 delay=8;
Так страница загружается без задержек, а клиент, который начал долбить API, сначала замедляется и только потом получает отказ.
| Настройка | Пачка из 25 запросов при rate=10r/s | Где уместна |
|---|---|---|
| без burst | 1 проходит, 24 отклонены | почти нигде; разве что строгий лимит на вход |
burst=20 | 21 проходят с растущей задержкой до ~2 с, 4 отклонены | фоновые задачи, где задержка не важна |
burst=20 nodelay | 21 проходят сразу, 4 отклонены | API, веб-приложения |
burst=20 delay=8 | 9 сразу, 12 с задержкой, 4 отклонены | публичные страницы с риском парсинга |
Как отдавать 429 с понятным ответом для API?
По умолчанию limit_req_status и limit_conn_status равны 503. Для клиента и мониторинга это выглядит как падение сервиса, а не как превышение лимита. Код 429 Too Many Requests описан в RFC 6585: клиентские библиотеки понимают его и умеют делать паузу перед повтором.
Для API удобно вернуть JSON и заголовок Retry-After:
location /api/ {
limit_req zone=api burst=20 nodelay;
error_page 429 = @ratelimited;
proxy_pass http://127.0.0.1:8080;
}
location @ratelimited {
default_type application/json;
add_header Retry-After 1 always;
return 429 '{"error":"too_many_requests","retry_after":1}';
}
Параметр always нужен потому, что без него add_header не срабатывает для ответов с кодами ошибок.
Как ограничить разные эндпоинты по-разному?
Один общий лимит на весь сайт почти всегда неудачен: для /login он слишком мягкий, а для статики — слишком жёсткий. Заведите несколько зон под разные сценарии:
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=search:10m rate=2r/s;
server {
location = /login { limit_req zone=login burst=5 nodelay; proxy_pass http://app; }
location /api/ { limit_req zone=api burst=20 nodelay; proxy_pass http://app; }
location /api/search { limit_req zone=search burst=5; proxy_pass http://app; }
location /static/ { root /var/www; } # без лимита
}
На один location можно повесить несколько limit_req — сработает самый строгий. Например, лимит по IP плюс общий лимит на сервер, чтобы тяжёлый поиск не съел бэкенд целиком:
limit_req_zone $server_name zone=search_total:1m rate=50r/s;
location /api/search {
limit_req zone=search burst=5;
limit_req zone=search_total burst=20;
proxy_pass http://app;
}
⚠️ Важная деталь наследования: директивы limit_req наследуются с верхнего уровня, только если на текущем уровне своих limit_req нет. Поставили общий лимит в server, а в одном location добавили свой — общий в этом location перестал действовать.
Ключ по API-токену вместо IP
За одним IP может сидеть офис из сотни человек или мобильный оператор с CGNAT, а один клиент API может ходить с разных адресов. Если клиенты передают ключ в заголовке, считайте по нему, а для анонимных запросов используйте IP:
map $http_x_api_key $api_limit_key {
"" $binary_remote_addr;
default $http_x_api_key;
}
limit_req_zone $api_limit_key zone=apikey:20m rate=20r/s;
Nginx не проверяет, что ключ настоящий: клиент может присылать каждый раз новый и обходить лимит. Поэтому такой лимит ставят вместе с проверкой ключа в приложении и с отдельным лимитом по IP.
Как ограничить одновременные соединения через limit_conn?
limit_conn ограничивает не частоту, а число соединений, которые открыты прямо сейчас. Он полезен там, где один запрос долгий: скачивание больших файлов, стриминг, медленные загрузки.
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_status 429;
location /downloads/ {
limit_conn perip 3;
limit_rate_after 10m;
limit_rate 2m;
}
Здесь один IP может качать не больше трёх файлов одновременно, а после первых 10 МБ скорость каждой загрузки ограничивается 2 МБ/с.
Учтите поведение HTTP/2 и HTTP/3: там соединение одно, а запросов в нём много, поэтому Nginx считает каждый одновременный запрос как отдельное соединение. Лимит limit_conn perip 3 для страницы, которая по HTTP/2 параллельно тянет 30 ресурсов, окажется слишком жёстким — вешайте его на конкретные тяжёлые location, а не на весь сервер.
Как не ограничить свой мониторинг и доверенные адреса?
Если ключ лимита пустой, Nginx запрос не учитывает. На этом построен стандартный белый список через geo и map:
geo $limit_exempt {
default 0;
10.0.0.0/8 1; # внутренняя сеть
192.0.2.15 1; # Prometheus
198.51.100.7 1; # офис
}
map $limit_exempt $limit_key {
0 $binary_remote_addr;
1 "";
}
limit_req_zone $limit_key zone=api:10m rate=10r/s;
Отдельно выведите из-под лимитов health check балансировщика: при всплеске трафика он получит 429 и выведет здоровый узел из ротации — авария на ровном месте. Проще всего отдавать /healthz из отдельного location без limit_req. Как собирать метрики сервера, разобрано в статье про Prometheus и Grafana.
Что делать, если Nginx стоит за балансировщиком или CDN?
Восстановить реальный IP клиента
За прокси $binary_remote_addr — это адрес балансировщика, а не клиента. Все пользователи попадают в одно ведро, и лимит срабатывает на всех сразу. Восстановите реальный IP модулем realip:
set_real_ip_from 10.0.0.0/8; # адреса ваших балансировщиков
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Указывайте в set_real_ip_from только свои прокси. Если доверять заголовку от любого источника, клиент подставит произвольный X-Forwarded-For и будет обходить лимит. Для CDN используйте их официальные списки адресов и обновляйте их по расписанию. Как устроены сами балансировщики, мы разбирали в материале про Traefik, Nginx и HAProxy.
Учесть несколько узлов Nginx
Зона общая для воркеров одного экземпляра Nginx, но не для разных серверов. При трёх узлах за балансировщиком клиент фактически получает тройной лимит. Варианты:
- делить
rateна число узлов; - включить на балансировщике привязку клиента к узлу;
- переносить строгие квоты в приложение с общим хранилищем — счётчики в Redis для этого подходят хорошо, подробнее в статье про Redis на Linux.
Как проверить лимиты, никого не задев?
Сначала неделю держите лимит в режиме dry_run и смотрите по логам, кого он задел бы, затем проверьте его нагрузкой со своего адреса, который не входит в белый список.
Режим dry_run
С версии 1.17.1 есть limit_req_dry_run on;: Nginx считает запросы и пишет в лог, кого бы отклонил, но ничего не блокирует. С 1.17.6 доступна переменная $limit_req_status со значениями PASSED, DELAYED, REJECTED, DELAYED_DRY_RUN и REJECTED_DRY_RUN. Добавьте её в формат лога:
log_format rl '$remote_addr [$time_local] "$request" $status rl=$limit_req_status';
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_dry_run on;
access_log /var/log/nginx/api_rl.log rl;
proxy_pass http://app;
}
Через неделю посмотрите, кого лимит задел бы:
grep REJECTED_DRY_RUN /var/log/nginx/api_rl.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
Если в списке ваш офис, мобильное приложение или партнёр — расширьте лимит или белый список и только потом уберите dry_run. Для limit_conn есть такой же limit_conn_dry_run.
Нагрузочная проверка
Быстрая проверка из терминала — посчитать коды ответов на пачку запросов:
for i in $(seq 1 40); do
curl -s -o /dev/null -w '%{http_code}\n' https://api.example.com/api/ping
done | sort | uniq -c
При rate=10r/s burst=20 nodelay ожидайте примерно 21 ответ 200 и остальные 429: часть мест в ведре освобождается, пока идёт цикл. Для параллельной нагрузки подойдут ab -n 200 -c 20 из пакета apache2-utils или wrk. Тестируйте со своего адреса, который не входит в белый список, — иначе получите одни 200 и ложную уверенность.
Как блокировать тех, кто постоянно упирается в лимит?
limit_req отклоняет запросы, но не банит: бот продолжит слать их бесконечно и засорять логи. Отклонения пишутся в error.log строками с limiting requests, а в fail2ban для них уже есть готовый фильтр nginx-limit-req:
# /etc/fail2ban/jail.d/nginx-limit-req.local
[nginx-limit-req]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
findtime = 600
maxretry = 30
bantime = 3600
sudo systemctl restart fail2ban
sudo fail2ban-client status nginx-limit-req
Тридцать отказов за десять минут — бан на час на уровне файрвола, Nginx этих запросов больше не видит. Следите за limit_req_log_level: если поставить его ниже уровня, который пишет error_log, строки в лог не попадут и fail2ban ничего не увидит. Базовые правила файрвола описаны в статье о защите Linux-сервера, а сбор и анализ логов — в материале о логировании в продакшене.
Какие ошибки с rate limiting встречаются чаще всего?
- Лимит без burst. Обычные пользователи получают отказы при открытии страницы. Почти всегда нужен
burstсnodelayилиdelay. - Оставленный 503. Мониторинг поднимает тревогу «бэкенд упал», а клиенты не понимают, что надо подождать.
- Реальный IP не восстановлен. За балансировщиком все клиенты делят одно ведро.
- Доверие X-Forwarded-For от всех. Лимит обходится одной строкой в заголовке.
- Лимит на health check. Балансировщик выкидывает здоровые узлы при пике.
- Сломанное наследование. Свой
limit_reqвlocationотменил общий изserver. - Слишком маленькая зона. Под нагрузкой она переполняется и сама начинает отклонять запросы.
- Включение сразу в бою. Без недели в
dry_runпервым под лимит обычно попадает свой же сервис. - Надежда на Nginx как на защиту от DDoS. Распределённую атаку с тысяч адресов и забитый канал лимит не остановит — это работа провайдера или CDN.
Готовая конфигурация для API
Собранный пример, который можно взять за основу. Подробности по каждой директиве — в официальной документации модуля limit_req.
# http {}
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
geo $limit_exempt { default 0; 10.0.0.0/8 1; }
map $limit_exempt $limit_key { 0 $binary_remote_addr; 1 ""; }
limit_req_zone $limit_key zone=api:10m rate=10r/s;
limit_req_zone $limit_key zone=login:10m rate=5r/m;
limit_conn_zone $limit_key zone=perip:10m;
limit_req_status 429;
limit_conn_status 429;
limit_req_log_level warn;
# server {}
location = /healthz { access_log off; return 200 "ok\n"; }
location = /api/login {
limit_req zone=login burst=5 nodelay;
error_page 429 = @ratelimited;
proxy_pass http://app;
}
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_conn perip 20;
error_page 429 = @ratelimited;
proxy_pass http://app;
}
location @ratelimited {
default_type application/json;
add_header Retry-After 1 always;
return 429 '{"error":"too_many_requests"}';
}
Если вы ещё выбираете веб-сервер под новый проект, загляните в сравнение Nginx и Apache: в Apache штатный mod_ratelimit ограничивает скорость отдачи ответа, а не частоту запросов, поэтому для лимита запросов там ставят сторонний mod_evasive.
Частые вопросы о rate limiting в Nginx
Какой лимит запросов ставить для API?
Универсального числа нет. Возьмите из логов 99-й перцентиль запросов в секунду от одного клиента в обычный день, умножьте на 2–3 и задайте это как rate, а burst — на размер типичной пачки запросов при открытии страницы. Первую неделю держите лимит в режиме limit_req_dry_run и смотрите, кого он задел бы.
Почему Nginx отдаёт 503, а не 429?
Так работает значение по умолчанию: limit_req_status и limit_conn_status равны 503. Добавьте limit_req_status 429; и limit_conn_status 429; на уровне http или server, чтобы клиенты и мониторинг отличали превышение лимита от падения бэкенда.
Спасает ли limit_req от DDoS?
От флуда с одного или нескольких адресов, перебора паролей и агрессивных парсеров — да. От распределённой атаки с тысяч адресов или от забивания канала — нет: трафик уже дошёл до сервера. Против таких атак нужна фильтрация у провайдера или CDN до вашего Nginx.
Что будет, если зона limit_req_zone переполнится?
Nginx удаляет самые давно не использованные состояния. Если места всё равно не хватает, новый запрос получает ошибку с кодом limit_req_status. На 64-битной системе 1 МБ вмещает около 8 тысяч состояний по ключу $binary_remote_addr, поэтому зоны 10m хватает примерно на 80 тысяч адресов.
Работает ли лимит, если у меня несколько серверов Nginx?
Каждый экземпляр Nginx считает запросы в своей разделяемой памяти, общей только для его воркеров. При трёх узлах за балансировщиком клиент фактически получает тройной лимит. Либо делите rate на число узлов, либо переносите строгие квоты в приложение с общим хранилищем вроде Redis.
Нужно ли ограничивать health check и мониторинг?
Нет. Адреса балансировщика, Prometheus и внутренних сервисов выносят в белый список через geo и map с пустым ключом либо отдают /healthz из отдельного location без limit_req. Иначе при всплеске трафика балансировщик получит 429 и выведет здоровый узел из ротации.