Архив рассылок сообщества снова открыт. Читать письма 2003-2009
Главная/Блог/Безопасность
Безопасность16 мин чтенияНиколай Ковалёв

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 бит)Подходит для
1m8 000внутренний сервис, админка
10m80 000обычный сайт или API
50m400 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Где уместна
без burst1 проходит, 24 отклоненыпочти нигде; разве что строгий лимит на вход
burst=2021 проходят с растущей задержкой до ~2 с, 4 отклоненыфоновые задачи, где задержка не важна
burst=20 nodelay21 проходят сразу, 4 отклоненыAPI, веб-приложения
burst=20 delay=89 сразу, 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 встречаются чаще всего?

  1. Лимит без burst. Обычные пользователи получают отказы при открытии страницы. Почти всегда нужен burst с nodelay или delay.
  2. Оставленный 503. Мониторинг поднимает тревогу «бэкенд упал», а клиенты не понимают, что надо подождать.
  3. Реальный IP не восстановлен. За балансировщиком все клиенты делят одно ведро.
  4. Доверие X-Forwarded-For от всех. Лимит обходится одной строкой в заголовке.
  5. Лимит на health check. Балансировщик выкидывает здоровые узлы при пике.
  6. Сломанное наследование. Свой limit_req в location отменил общий из server.
  7. Слишком маленькая зона. Под нагрузкой она переполняется и сама начинает отклонять запросы.
  8. Включение сразу в бою. Без недели в dry_run первым под лимит обычно попадает свой же сервис.
  9. Надежда на 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 и выведет здоровый узел из ротации.

Вернуться в блог