В этом уроке разберём nginx rate limit вглубь: как устроен limit_req_zone и limit_req, что на самом деле делают burst, nodelay и delay (это не магия, это конкретный алгоритм), чем ограничение запросов отличается от ограничения соединений (limit_conn), почему nginx 429 лучше дефолтного 503, как лимитировать не только по IP, но и по пользователю или API-ключу, и почему ключ по IP врёт за NAT и без realip. В конце соберём многоуровневую защиту логина и API ровно по эталонному global_rate_limit.conf.
Алгоритм leaky bucket: как nginx ограничение запросов считает на самом деле
В основе nginx limit_req лежит leaky bucket - "дырявое ведро". Образ простой. Есть ведро с дыркой в дне. Запросы капают в ведро сверху неравномерно (всплески, провалы), а вытекают через дырку строго с постоянной скоростью - это твой rate. Пока вытекает быстрее, чем капает, ведро пустое и всё проходит. Если капает быстрее, чем вытекает, уровень растёт. Когда ведро переполнилось - лишние капли проливаются мимо, то есть запрос отвергается.
Важно понять, что rate задаёт именно скорость утечки, а не "сколько запросов в окне". Скажем, rate=5r/s nginx внутри пересчитывает в "один запрос раз в 200 миллисекунд". Это ключевой момент: лимит не "5 штук за любую секунду", а равномерный поток с шагом 200 мс. Если ты пришлёшь два запроса с интервалом 50 мс - второй уже формально превышает, потому что между ними должно было пройти 200 мс. Без настройки всплесков второй запрос немедленно отвергается. Поэтому голый limit_req без burst почти всегда слишком жёсткий для реального трафика - браузер легко шлёт 6-8 параллельных запросов на страницу.
Зона ограничения живёт в shared memory - общей памяти, видимой всем worker-процессам. Это критично: если бы счётчик был локальным для воркера, при worker_processes auto и десятке воркеров клиент получил бы лимит, умноженный на число воркеров. Shared-зона делает счёт честным на весь инстанс nginx.
Код: Выделить всё
# контекст http {} - тут объявляются зоны
limit_req_zone $binary_remote_addr zone=demo:10m rate=5r/s;
server {
location /api/ {
limit_req zone=demo; # без burst - максимально строго
proxy_pass http://backend;
}
}

burst, nodelay и delay: настройка всплесков в nginx limit_req
Чистый leaky bucket без буфера непригоден для веба - он рубит любой нормальный всплеск. Поэтому у ведра есть ёмкость сверх дырки, и задаёт её параметр burst. burst=N означает: разреши накопиться в очереди ещё до N запросов сверх равномерного потока. Дальше начинаются три разных поведения.
1. burst без модификаторов (режим задержки). Лишние запросы в пределах burst не отвергаются, а ставятся в очередь и выпускаются к бэкенду по графику rate. То есть всплеск размазывается во времени.
Код: Выделить всё
limit_req zone=demo burst=10;
2. burst + nodelay (мгновенный всплеск). Это самый частый продакшен-режим.
Код: Выделить всё
limit_req zone=demo burst=10 nodelay;
3. burst + delay (гибрид). delay=M говорит: первые M запросов сверх rate пропускай мгновенно, а вот запросы от M до burst - притормаживай по графику. Всё, что за burst - отвергай.
Код: Выделить всё
limit_req zone=demo burst=12 delay=8;
Запомни главный водораздел: nodelay - "пропусти всплеск целиком и сразу, лишнее руби", delay - "часть сразу, часть притормози, остаток руби", без модификатора - "всё притормози по графику". Для логинов и API почти всегда нужен nodelay.
limit_conn и nginx 429: соединения, статусы и полоса
limit_req считает запросы. Но есть второй вектор атаки - соединения. Slowloris и подобные техники не шлют много запросов, они открывают сотни соединений и держат их полуоткрытыми, медленно отправляя байты, чтобы выжрать все worker_connections. Тут поможет nginx limit_conn.
Код: Выделить всё
# http {}
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
server {
location /download/ {
limit_conn conn_per_ip 10; # не больше 10 одновременных соединений с одного IP
proxy_pass http://files;
}
}
Теперь про статус. По умолчанию обе директивы при отказе отдают 503 Service Unavailable. Это семантически неверно: 503 означает "сервер сам сломался", его кэшируют, на него по-другому реагируют клиенты и мониторинг. Правильный ответ на превышение лимита - 429 Too Many Requests. Меняется одной строкой:
Код: Выделить всё
limit_req_status 429;
limit_conn_status 429;
Рядом стоит limit_rate - это не про количество запросов, а про полосу отдачи одного соединения в байтах в секунду. Полезно, чтобы один клиент не выкачал весь канал.
Код: Выделить всё
location /download/ {
limit_rate 1m; # 1 МБ/с на соединение
limit_rate_after 10m; # первые 10 МБ - на полной скорости, дальше режем
}
Что в логах? При срабатывании nginx пишет в error_log строку вида "limiting requests, excess: ... by zone ...". Это твой главный диагностический сигнал - по нему видно, какая зона режет и насколько превышение. На этапе внедрения политики бесценна директива limit_req_dry_run on - она не блокирует, а только логирует превышения. Включаешь в бою, неделю смотришь, кого бы ты порезал, подкручиваешь лимиты, и только потом выключаешь dry_run. Так ты не положишь легитимных клиентов из-за слишком жадного лимита.
Семь боевых зон эталона: nginx ддос защита по IP, пользователю и ключу
Одной зоны на весь сайт мало. Логин надо защищать жёстко, публичное API - мягче, а внутренние сервисы почти не трогать. Эталонный global_rate_limit.conf держит семь зон под разные задачи и - что важнее - под разные ключи. Вот они целиком.
Сначала готовятся безопасные ключи через map (это в http{}, чтобы переменные были видны во всех нижележащих конфигах):
Код: Выделить всё
# идентификатор потребителя API из заголовка, иначе "anon"
map $http_x_consumer_id $api_consumer_id {
default $http_x_consumer_id;
"" "anon";
}
# API-ключ из заголовка, иначе "anon"
map $http_x_api_key $rl_api_key {
default $http_x_api_key;
"" "anon";
}
Код: Выделить всё
# 1) чувствительные операции (логин/пароль/2FA) - жёсткий лимит по IP
limit_req_zone $binary_remote_addr zone=app_login:10m rate=5r/s;
# 2) security-critical (выдача SMS-кода) - очень жёстко
limit_req_zone $binary_remote_addr zone=security_sensitive:5m rate=1r/s;
# 3) публичный API по IP - мягкий типовой REST
limit_req_zone $binary_remote_addr zone=public_api_ip:20m rate=60r/m;
# 4) публичный API по пользователю (X-Consumer-Id)
limit_req_zone $api_consumer_id zone=public_api_user:20m rate=120r/m;
# 5) публичный API по API-ключу (X-Api-Key)
limit_req_zone $rl_api_key zone=public_api_key:20m rate=10r/s;
# 6) админка по IP
limit_req_zone $binary_remote_addr zone=admin_area:5m rate=10r/s;
# 7) внутренние клиенты по IP - щадящий лимит против runaway-клиента
limit_req_zone $binary_remote_addr zone=internal_clients:10m rate=200r/s;
- По IP ($binary_remote_addr) - грубая защита от лобовой атаки. Зоны 1, 2, 3, 6, 7. Хорошо ловит ботнет с ограниченного числа адресов и тупой перебор. Плохо работает там, где много клиентов за одним IP (NAT, корпоративный прокси, мобильный оператор) - об этом ниже.
- По пользователю ($api_consumer_id) - зона 4. Считает запросы конкретного аккаунта независимо от того, с какого IP он ходит. Усмиряет "шумного" легитимного клиента, который из-за бага в своём коде долбит в цикле. По IP его не поймать - он может быть за CDN с тысячей адресов.
- По API-ключу ($rl_api_key) - зона 5. Лимит на выданный токен. Удобно для тарификации и для того, чтобы один скомпрометированный ключ не утащил весь бэкенд. Здесь rate в r/s строже, потому что машинный клиент по ключу легко обязан укладываться в ровный поток.
Код: Выделить всё
limit_req_status 429;
location = /login {
limit_req zone=app_login burst=5 nodelay; # IP-защита логина
proxy_pass http://auth_backend;
}
location ^~ /api/ {
limit_req zone=public_api_ip burst=120 delay=60; # грубо по IP
limit_req zone=public_api_user burst=240 delay=120; # тонко по клиенту
proxy_pass http://backend_api;
}
Грабли: ключ по IP врёт за NAT и без realip, и как их обойти
Главная ловушка nginx ограничения запросов по IP - неправильный $remote_addr. Если nginx стоит за CDN, балансировщиком L7 или другим прокси, то $remote_addr (и значит $binary_remote_addr) - это IP ближайшего прокси, а не клиента. Последствие катастрофическое: весь трафик планеты приходит с пары адресов CDN, все клиенты схлопываются в один ключ, и зона app_login со своими 5r/s порежет вообще всех пользователей сразу. Лекарство - модуль realip (подробно был в уроке 11):
Код: Выделить всё
# доверяем ТОЛЬКО известным сетям своего CDN/балансировщика
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;
Вторая грабля - NAT и общий IP. Даже с идеальным realip за одним публичным адресом мобильного оператора или офиса сидят сотни реальных людей. Жёсткий IP-лимит покарает их всех скопом из-за одного абьюзера. Чистого решения на уровне IP нет - именно поэтому в эталоне для API есть зоны по пользователю и по ключу. Где можешь идентифицировать клиента точнее IP (аккаунт, токен, сессия) - лимитируй по нему, IP оставляй как грубый внешний контур.
Третья грабля - белые списки. Свой мониторинг, healthcheck-пробы Kubernetes, партнёрские интеграции не должны попадать под лимит. Делается это через geo + map: для доверенных сетей ключ зоны обнуляется в пустую строку, а пустой ключ, как мы помним, не лимитируется.
Код: Выделить всё
geo $limit_whitelisted {
default 0;
10.0.0.0/8 1; # внутренняя сеть
203.0.113.7/32 1; # партнёр
}
map $limit_whitelisted $limit_key {
0 $binary_remote_addr; # обычные - лимитируем по IP
1 ""; # белый список - пустой ключ = без лимита
}
limit_req_zone $limit_key zone=public:20m rate=60r/m;
За пределами одного запроса: связка с fail2ban
limit_req режет частоту, но не выгоняет злодея. Бот, упёршийся в 429, продолжит долбить - просто будет получать отказы, всё равно тратя коннекты и место в логах. Логично после N срабатываний лимита банить источник на сетевом уровне. Это работа fail2ban: он читает логи nginx, ловит шаблон (например строки про 429 или "limiting requests" в error_log) и временно закрывает IP через nftables/iptables. Запрос такого клиента вообще не дойдёт до nginx - дёшево и эффективно.
Код: Выделить всё
# /etc/fail2ban/filter.d/nginx-429.conf
[Definition]
failregex = ^<HOST>.* 429
# jail.local
[nginx-429]
enabled = true
port = http,https
filter = nginx-429
logpath = /var/log/nginx/access.log
maxretry = 30
findtime = 60
bantime = 3600
Мини-лаба: многоуровневая защита логина и API руками
Поднимем защиту и проверим её нагрузкой. Нужен любой бэкенд-заглушка (хоть python3 -m http.server) и утилита ab или hey.
- 1. В http{} добавь зоны и статус:
Код: Выделить всё
limit_req_zone $binary_remote_addr zone=app_login:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=public_api_ip:20m rate=60r/m;
limit_req_status 429;
- 2. В server{} два location:
Код: Выделить всё
location = /login {
limit_req zone=app_login burst=5 nodelay;
proxy_pass http://127.0.0.1:8000;
}
location ^~ /api/ {
limit_req zone=public_api_ip burst=10 delay=5;
proxy_pass http://127.0.0.1:8000;
}
- 3. Проверь конфиг и перечитай: nginx -t && nginx -s reload
- 4. Бей по логину 50 параллельных запросов: ab -n 50 -c 50 http://localhost/login - в выводе ab часть ответов будет non-2xx. Это твои 429: 5 прошло по rate + 5 burst, остальные срублены.
- 5. Смотри живой error_log: tail -f /var/log/nginx/error.log - увидишь строки "limiting requests, excess: ... by zone "app_login"". Это и есть подтверждение работы.
- 6. Поменяй nodelay на delay=3 в /login, reload, повтори ab. Сравни время ответа - теперь часть запросов не отвергается, а задерживается, общий тест идёт дольше.
- 7. Включи limit_req_dry_run on; в server, reload, повтори шаг 4. Теперь 429 нет совсем (все запросы прошли), но в error_log срабатывания логируются. Это режим обкатки лимита без блокировок.
Контрольные вопросы
- 1. В чём разница между limit_req zone=z burst=10 (без модификатора), burst=10 nodelay и burst=10 delay=5 при rate=5r/s? Что произойдёт с пачкой из 20 одновременных запросов в каждом случае?
- 2. Почему лимитирование по $binary_remote_addr ломается, когда nginx стоит за CDN, и что именно нужно настроить, чтобы ключ снова считался честно? Какую новую дыру создаёт неаккуратная настройка этого механизма?
- 3. Зачем в эталоне отдельные зоны public_api_user (ключ $api_consumer_id) и public_api_key (ключ $rl_api_key), если уже есть public_api_ip? Какой сценарий ловит каждая и не ловят другие?
- 4. Чем limit_conn отличается от limit_req по смыслу, и почему limit_rate без limit_conn легко обойти?
Rate limiting в nginx - это leaky bucket: rate задаёт скорость утечки, burst - ёмкость для всплесков, а nodelay/delay/без-модификатора определяют, пропускать всплеск мгновенно, размазывать или отвергать. limit_req режет частоту запросов, limit_conn - число соединений, limit_rate - полосу; при отказе отдавай nginx 429, а не 503. Сила приёма - в правильном ключе: по IP (грубый контур, врёт за NAT и без realip), по пользователю и по API-ключу (точное усмирение шумных клиентов). Эталонные семь зон - готовая палитра под логин, security-операции, публичное API и внутренние сервисы. А упорных абьюзеров после 429 добивает fail2ban на сетевом уровне. Это твой первый и обязательный рубеж обороны шлюза - но помни, что от настоящего объёмного DDoS он лишь часть эшелона.