Ви ввімкнули обмеження швидкості, графіки заспокоїлись, а потім служба підтримки загорілась повідомленнями: «користувачі не можуть увійти». Бот-трафік зник. Разом з ним зникли й люди. Ласкаво просимо до класичної пастки Nginx: увімкнути rate limiting легко, а налаштувати його так, щоб він відповідав реальній поведінці користувачів, — набагато складніше.
Це довідник, орієнтований на продакшн, для систем Ubuntu 24.04 з Nginx. Ми виберемо розумні ключі, порахуємо розмір зон спільної пам’яті, опрацюємо сплески без заохочення зловмисників і навчимось діагностувати 429 без здогадок. Ви отримаєте конфіги, які можна захищати в рев’ю змін, та план дій, який можна виконати о 02:00 з напівзаплющеними очима.
Що насправді робить обмеження швидкості Nginx (і чого не робить)
Nginx має два основні регулятори, які люди зазвичай зводять в одне поняття «rate limiting»:
limit_req: обмежує швидкість запитів за ключем (запитів за секунду/хвилину). Думайте «з якою швидкістю цей клієнт робить запити?»limit_conn: обмежує кількість одночасних зʼєднань за ключем. Думайте «скільки сокетів цей клієнт тримає відкритими?»
limit_req використовують для захисту від брутфорсу, забезпечення чесності API та помʼякшення простих DDoS-розпилів. limit_conn — для поведінки на кшталт slowloris, надмірно розмовних клієнтів або помилки CDN/моніторингу, що відкриває забагато зʼєднань.
Чого жоден з них не робить: вони не розрізняють «реальних користувачів» і «фейкових користувачів». Вони виконують політику. Якщо ваш ключ політики неправильний або пороги ігнорують сучасну веб-поведінку (паралельні запити, повторні спроби, мобільні мережі), ви заблокуєте саме тих людей, заради яких вам платять.
Також: обмеження швидкості — це не WAF, не виявлення ботів і не заміна механізмів аутентифікації. Це запобіжник для шаблонів обсягу запитів.
Кілька фактів і історія, що спрощують налаштування
Це не дрібниці заради дрібниць. Це причини, чому правило «5r/s на IP» працює в одному середовищі і вибухає в іншому.
- HTTP/1.1 keepalive змінив значення «зʼєднань». Раніше HTTP відкривав багато коротких зʼєднань; keepalive дозволив одному зʼєднанню нести багато запитів. Саме тому
limit_connможе бути марним для хвиль запитів і катастрофічним для WebSocket. - HTTP/2 мультиплексування змінило значення «паралельних». Браузери можуть виконувати багато одночасних стрімів в одному TCP-зʼєднанні. Клієнт може генерувати сплески без відкриття додаткових сокетів, тому контролі за швидкістю запитів набувають більшого значення, ніж ліміти зʼєднань.
- CDN та carrier-grade NAT зʼєднали користувачів під спільними IP. Лімітування за IP може карати цілі офіси, школи, готелі та пул вихідних ip мобільних операторів. Це не теоретичний крайній випадок; це вівторок.
- Nginx зберігає стан лімітації в зоні спільної пам’яті. Це швидко, бо в пам’яті й спільно між воркерами. Але ця зона обмежена; коли вона заповнюється, відбувається вилучення старих записів. Вилучення змінює поведінку під навантаженням.
- Модель «витікаючого відра» стара, але все ще виграє.
limit_req— це лімітер у стилі токен/витікання. Він передбачуваний і дешевший за зовнішні перевірки на кожен запит. - 429 Too Many Requests — це сучасний контракт. Це не просто «заблоковано». Багато клієнтів повторять спробу; деякі відступлять; деякі битимуть далі. Повернення 429 або 503 змінює форму трафіку.
- Повторні спроби стали нормою. Мобільні клієнти, service mesh і бібліотеки агресивно повторюють запити. «Невелике уповільнення» може перетворитися на сплеск повторів, який підриває ваш ліміт і погіршує ситуацію.
- Атакувальники адаптувалися до наївних лімітів роки тому. Ботнети міняють IP; крашери облікових даних розподіляють спроби по багатьох джерелах; скрейпери використовують резидентні проксі. Один глобальний ліміт за IP — здебільшого лише перешкода.
Цитата, що має бути в кожному вішборді он-колу: Надія — не стратегія.
— James Cameron. Коротко, різко і правдиво.
Принципи: не лімітуйте «користувачів», лімітуйте поведінки
Якщо ви намагаєтесь захистити «вебсайт» одним лімітером, ви або будете надто суворі до людей, або занадто м’які для зловмисників. Натомість:
- Захищайте дорогі дії більше, ніж дешеві. Спроби входу, скидання паролів, пошук та динамічні сторінки отримують суворіші ліміти. Статичні ресурси можна практично не обмежувати (або віддати CDN).
- Краще використовувати ключі ідентичності, ніж мережеві ключі, коли це можливо. Лімітуйте за API-ключем, ID користувача або сесією для автентифікованого трафіку. IP — останній варіант для «невідомого» трафіку.
- Дозвольте сплески, але обмежуйте сталий зловживання. Люди клікають, додатки підвантажують, браузери відкривають паралельні запити, а JS виконує фонові виклики. Невеликий запас для сплесків запобігає хибним спрацьовуванням.
- Поверніть «ввічливо» для клієнтів, які повторюють спроби. Якщо ви повертаєте 429, додавайте, де можливо, заголовок
Retry-After. Ви формуєте трафік, а не просто закриваєте двері. - Зробіть усе спостережуваним. Якщо ви не можете відповісти на питання «хто був обмежений, на якому ендпоінті, з яким ключем і чи це було очікувано?», ви керуєте наосліп.
Жарт 1/2: обмеження швидкості — як охоронець нічного клубу: хороші зупиняють бійку, погані виганяють бухгалтера, бо він «виглядав підозріло».
Вибір правильного ключа: IP, користувач, токен чи щось розумніше
Ключ — це серце вашого лімітеру. Він визначає «кого» рахувати. Більшість проблем походять від вибору ключа, що не відповідає реальності вашого трафіку.
Опція A: $binary_remote_addr (за IP)
Це шаблон за замовчуванням, бо простий і не вимагає змін у додатку.
- Переваги: Працює для анонімного трафіку; дешевий; без змін у додатку.
- Недоліки: NAT і проксі з’єднують кількох користувачів в один відро; обертові IP уникають його; за балансувальником ви можете обмежувати сам LB, якщо не налаштований реальний IP.
Використовуйте його для: захисту від брутфорсу на публічних ендпоінтах, базового захисту для неавтентифікованих ділянок, грубої міграції під час інциденту.
Опція B: ідентичність автентифікованого користувача (кукі, JWT-поле, API-ключ)
Якщо у вас є заголовок API-ключа, наприклад X-API-Key, або JWT-поле, яке можна відобразити у змінну, ви можете лімітувати на клієнта замість NAT-шлюзу.
- Переваги: Справедливо; стійко до проблем NAT; узгоджується з межами білінгу/зловживань.
- Недоліки: Потрібно надійне парсування; для неавтентифікованих шляхів потрібен fallback; скомпрометовані ключі можуть бути зловживані.
Опція C: композитні ключі (IP + клас шляху, або IP + UA)
Композитні ключі корисні, коли ви не можете довіряти ідентичності, але хочете розділити поведінки.
Приклад: обмежуйте спроби входу за IP, але також застосуйте загальний низький ліміт для одного IP, який вдаряє по багатьох іменах користувачів. Nginx не робить крос-кореляції між ключами за вас, але ви можете задати різні зони для різних місць і вибору ключа.
Що я насправді рекомендую в продакшні
Використовуйте два рівні:
- Анонімний рівень: обмеження за IP для чутливих ендпоінтів (логін, скидання паролю, пошук). Помірний burst. Строгий сталий ліміт.
- Автентифікований рівень: обмеження за клієнтом (токен/API-ключ) для API. Вищий burst. Вищий сталий ліміт. Окремі ліміти за підпискою, якщо можливо.
І якщо ви за балансувальником, спочатку налаштуйте реальний IP. Якщо цього не зробити, ви будете обмежувати балансувальник і називати це «безпекою».
Розмір зон спільної пам’яті та розуміння вилучення записів
Nginx зберігає стан лімітації в зоні спільної пам’яті, визначеній через limit_req_zone. Ця зона має фіксований розмір. Коли вона заповнюється, старі записи вилучаються. Вилучення викликає два види дивної поведінки:
- Недолікова лімітація: зловмисні клієнти переключаються між достатньою кількістю унікальних ключів, щоб витіснити свої записи з зони, фактично «забуваючи» свою історію.
- Надмірне обмеження невинних: менш поширене, але коли зона «трешиться», ви можете бачити нерівномірну поведінку, де клієнти інколи проходять, інколи отримують 429, залежно від того, чи їх запис наразі присутній.
Документація Nginx часто пропонує грубі оцінки пам’яті на запис, але на практиці ви плануєте, виходячи з унікальних ключів, яких очікуєте під час піків. Для ліміту за IP на публічному сайті унікальних ключів може бути приголомшливо багато.
Практична евристика розміру
Почніть з:
- 10m для невеликих сайтів або однопроцесних ендпоінтів
- 50m–200m для публічних високонавантажених ребер
Потім вимірюйте. Якщо ви запускаєте кілька зон (рекомендовано), кожна зона потребує свого розміру.
Burst, nodelay і чому «плавність» не завжди корисна
Більшість хибних спрацьовувань відбуваються через сплески. Реальні браузери й мобільні додатки не роблять по одному запиту на секунду, як чемні роботи. Вони роблять це:
- Завантажують HTML
- Негайно запитують CSS, JS, зображення, шрифти
- Коли JS завантажено, виконують API виклики (іноді кілька)
- Швидко повторюють невдалий запит
limit_req підтримує burst і nodelay:
burst=Nдозволяє N зайвих запитів поставити в чергу (або затримати), згладжуючи сплеск.nodelayдозволяє сплескам проходити негайно до межі burst (без затримки); понад це — запити відкидаються.
Компроміс:
- burst без nodelay: добріше для бекенду, але користувачі відчують затримку і можуть повторити запит, що може посилити навантаження.
- burst з nodelay: кращий UX для невеликих сплесків, але може дати більш різкий удар по upstream під час атак.
Для ендпоінтів входу я часто віддаю перевагу без nodelay (затримувати зловмисників), але з невеликим burst для випадкових подвійних кліків. Для API, де клієнти таймаутять і повторюють, я часто обираю nodelay з помірним burst, щоб «нормальні» мікросплески не перетворювалися на довготривалу затримку.
Різні ліміти для різних шляхів (логіни — не CSS)
Перестаньте застосовувати один лімітер до / і називати це робочим рішенням. Вам потрібні політики за класами:
- Вхід / автентифікація: суворо за IP, дуже суворо за імʼям користувача, якщо додаток це підтримує (Nginx сам по собі не робить цього просто, бо імʼя в POST-тілі). Додавайте виключення за сесією обережно.
- Скидання паролю / OTP: суворо. Зловживання тут коштує грошей і репутації.
- Пошук / дорогі запити: суворо за IP або за токеном, бо боти полюбляють пошук.
- Загальне API: за API-ключем/клієнтом. Різні рівні, якщо можете.
- Статичні ресурси: зазвичай без
limit_req; використовуйте CDN-кешування або високі ліміти і зосередьтесь на контролі зʼєднань. - WebSocket / SSE: не використовуйте обмеження швидкості запитів після апгрейду; розгляньте ліміти зʼєднань за IP або токеном.
За балансувальником: реальний IP, межі довіри та підробка
На Ubuntu 24.04 Nginx часто стоїть за хмарним балансувальником, ingress-контролером, CDN або service mesh. Якщо ви бездумно використовуєте $remote_addr, ви можете обмежувати адресу проксі. Це означає:
- Один галасливий клієнт може задушити всіх.
- Або ви ставите ліміти такими високими, що вони неефективні, і атакувальники проходять повз.
Виправте це за допомогою модуля Real IP, але будьте ретельні щодо довіри. Приймайте X-Forwarded-For (або PROXY protocol) лише від відомих IP-діапазонів ваших проксі. Якщо ви довіряєте всьому Інтернету, будь-хто може підробити X-Forwarded-For і отримати нескінченні свіжі ідентичності.
Логування та спостережуваність: зробіть 429 дієвими
Формат логів за замовчуванням не скаже, чому запит був обмежений. Вам потрібно логувати:
- ключ, що використовувався для ліміту (або принаймні IP клієнта після обробки real-IP)
- час запиту та час upstream
- код статусу та байти, що віддані
- request ID для кореляції
- який лімитер спрацював
Nginx надає $limit_req_status, що надзвичайно корисно. Логуйте його. Якщо воно «REJECTED», ви знаєте, що спрацював лімитер. Якщо «PASSED», але клієнти все одно скаржаться, вузьке місце — не тут.
Практичні завдання: команди, виводи та рішення (12+)
Ось ті речі, які я реально виконую на Ubuntu, коли кажуть «rate limiting блокує реальних користувачів» або «сайт під навалом». Кожне завдання включає, що означає вивід і яке рішення на його підставі прийняти.
Завдання 1: Підтвердити версію Nginx і зібрані модулі
cr0x@server:~$ nginx -V
nginx version: nginx/1.24.0 (Ubuntu)
built with OpenSSL 3.0.13 30 Jan 2024
configure arguments: ... --with-http_realip_module --with-http_limit_req_module --with-http_limit_conn_module ...
Що це означає: У вас скомпільовані модулі Real IP, limit_req і limit_conn (звично для пакетів Ubuntu).
Рішення: Якщо --with-http_realip_module відсутній і ви стоїте за проксі, зупиніться і виправте стратегію пакування (використайте пакет дистрибутива або перебудуйте). Без real IP вибір ключа може бути марним.
Завдання 2: Перевірити активний конфіг (не той, що здається активним)
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Що це означає: Синтаксис вірний.
Рішення: Якщо це падає, не перезавантажуйте. Виправте конфіг перш ніж продовжувати. Зміни лімітерів, які не завантажуються коректно, стають «ми щось змінили» фольклором без реального ефекту.
Завдання 3: Вивантажити повний завантажений конфіг, щоб знайти include і override
cr0x@server:~$ sudo nginx -T | sed -n '1,120p'
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# configuration file /etc/nginx/nginx.conf:
user www-data;
worker_processes auto;
...
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
Що це означає: Тепер ви можете знайти, де визначені limit_req і limit_req_zone.
Рішення: Якщо ви знайдете кілька конфліктних лімітерів у conf.d і sites-enabled, консолідуйте їх. Розрізнені правила лімітування — це те, як у вас виходить «в стагінгу працює», а «в проді блокує».
Завдання 4: Перевірити, чи ви обмежуєте балансувальник (перевірка real IP)
cr0x@server:~$ sudo tail -n 5 /var/log/nginx/access.log
10.10.5.12 - - [30/Dec/2025:10:10:12 +0000] "GET /login HTTP/2.0" 200 512 "-" "Mozilla/5.0 ..."
10.10.5.12 - - [30/Dec/2025:10:10:12 +0000] "GET /api/me HTTP/2.0" 429 169 "-" "Mozilla/5.0 ..."
10.10.5.12 - - [30/Dec/2025:10:10:13 +0000] "GET /api/me HTTP/2.0" 429 169 "-" "Mozilla/5.0 ..."
Що це означає: Якщо всі клієнти виглядають як одна й та сама RFC1918 адреса (наприклад, 10.10.5.12), то це, ймовірно, ваш проксі/LB.
Рішення: Виправте Real IP перед тим, як торкатись порогів. Інакше ви налаштовуватимете під неправильного «клієнта» і заблокуєте всіх одночасно.
Завдання 5: Перевірити, чи налаштований Real IP і чи довіра вузька
cr0x@server:~$ sudo nginx -T | grep -E 'real_ip_header|set_real_ip_from|real_ip_recursive'
real_ip_header X-Forwarded-For;
set_real_ip_from 10.10.0.0/16;
set_real_ip_from 192.0.2.10;
real_ip_recursive on;
Що це означає: Nginx замінить $remote_addr за XFF, але тільки якщо запит приходить з довірених діапазонів проксі.
Рішення: Якщо ви бачите set_real_ip_from 0.0.0.0/0;, ви фактично запросили підробку. Виправте це негайно. Ваше лімітування може бути обійдене заголовком.
Завдання 6: Перевірити error.log на сигнали limit_req
cr0x@server:~$ sudo grep -E 'limiting requests|limit_req' /var/log/nginx/error.log | tail -n 5
2025/12/30 10:10:13 [error] 12410#12410: *991 limiting requests, excess: 5.610 by zone "api_per_ip", client: 203.0.113.55, server: example, request: "GET /api/me HTTP/2.0", host: "example"
Що це означає: Nginx відкидає через зону api_per_ip і показує обчислений «excess». Це золото для налаштування.
Рішення: Якщо блокуються люди, вирішіть, чи ключ неправильний (NAT/проксі), чи швидкість занадто низька, чи burst занадто малий.
Завдання 7: Додати формат логів, що реєструє статус лімітеру (і перевірити, що працює)
cr0x@server:~$ sudo grep -R "limit_req_status" -n /etc/nginx | head
/etc/nginx/nginx.conf:35:log_format main_ext '$remote_addr $request_id $status $request $limit_req_status';
Що це означає: Тепер у логах записується, чи лімитер пропустив, затримав або відкинув запит.
Рішення: Якщо ви не бачите $limit_req_status для кожного запиту, ви неправильно діагностуватимете «429 через upstream» проти «429 через лімитер». Додайте його перед налаштуванням.
Завдання 8: Підтвердити, хто повертає 429 (Nginx чи upstream)
cr0x@server:~$ curl -s -D - -o /dev/null https://example/api/me | sed -n '1,12p'
HTTP/2 429
server: nginx
date: Tue, 30 Dec 2025 10:12:10 GMT
content-type: text/html
content-length: 169
Що це означає: Відповідь від Nginx (див. server: nginx). Якщо upstream також повертає 429, знадобляться додаткові заголовки/поля логів, щоб відрізнити їх.
Рішення: Якщо це Nginx — налаштовуйте Nginx. Якщо upstream — не тратьте час на регулювання limit_req.
Завдання 9: Швидко виміряти поточні швидкості запитів по IP клієнта
cr0x@server:~$ sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
8421 203.0.113.55
2210 198.51.100.27
911 203.0.113.101
455 192.0.2.44
Що це означає: Які IP найгарячіші в логах за вибіркове вікно (це не справжня швидкість, але швидка карта гарячих точок).
Рішення: Якщо кілька IP домінують, обмеження за IP можуть бути ефективні. Якщо багато IP з невеликими лічильниками — у вас розподілений трафік, і за IP обмеження не допоможуть.
Завдання 10: Перевірити активні зʼєднання і хто їх тримає
cr0x@server:~$ sudo ss -Htn state established '( sport = :443 )' | awk '{print $4}' | cut -d: -f1 | sort | uniq -c | sort -nr | head
120 203.0.113.55
48 198.51.100.27
11 192.0.2.44
Що це означає: Клієнти з багатьма встановленими TCP-зʼєднаннями до 443.
Рішення: Якщо один клієнт тримає сотні/тисячі зʼєднань, краще використовувати limit_conn, а не limit_req. Якщо зʼєднань мало, але запитів багато — зосередьтесь на limit_req і кешуванні.
Завдання 11: Перевірити, чи ввімкнено HTTP/2 (впливає на поведінку burst)
cr0x@server:~$ sudo nginx -T | grep -R "listen 443" -n /etc/nginx/sites-enabled | head
/etc/nginx/sites-enabled/example.conf:12: listen 443 ssl http2;
Що це означає: HTTP/2 мультиплексування діє.
Рішення: Очікуйте «сплесків» від нормальних браузерів. Збільшіть burst для фронтенд-ендпоінтів або звужуйте лімітування для дорогих шляхів.
Завдання 12: Відслідковувати 429 у часі з логів (дешевий тренд)
cr0x@server:~$ sudo awk '$9==429 {c++} END{print c+0}' /var/log/nginx/access.log
317
Що це означає: Лічильник відповідей 429 у поточному файлі логів.
Рішення: Якщо це число значне, потрібно класифікувати: чи це очікувано (боти) чи непередбачувано (клієнти)? Додайте розбиття по ендпоінтах далі.
Завдання 13: Розбити 429 за шляхами, щоб знайти, що саме ви блокуєте
cr0x@server:~$ sudo awk '$9==429 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
210 /api/me
61 /login
24 /search
12 /password/reset
Що це означає: Які ендпоінти обмежуються.
Рішення: Якщо /api/me обмежується і ваш SPA викликає його при кожному завантаженні сторінки, ваша базова політика занадто жорстка або ключ неправильний (NAT/проксі). Якщо /login сильно обмежено — можливо це правильно (credential stuffing), але перевірте скарги клієнтів.
Завдання 14: Підтвердити конфіг зони і швидкості (знайти політику, яку ви застосовуєте)
cr0x@server:~$ sudo nginx -T | grep -E 'limit_req_zone|limit_req[^_]' -n | head -n 30
45: limit_req_zone $binary_remote_addr zone=login_per_ip:20m rate=5r/m;
46: limit_req_zone $binary_remote_addr zone=api_per_ip:50m rate=10r/s;
112: limit_req zone=login_per_ip burst=3;
166: limit_req zone=api_per_ip burst=20 nodelay;
Що це означає: Ваш логін обмежено до 5 запитів на хвилину за IP. Ваш API — 10 запитів за секунду за IP. Це дуже різні політики.
Рішення: Вирішіть, чи ці числа відображають реальність. 10r/s за IP може бути занадто низьким, якщо існують NAT’овані корпоративні клієнти. 5r/m для логіну може бути занадто малим, якщо процес логіну викликає кілька auth-запитів.
Завдання 15: Безпечно перезавантажити і підтвердити, що воркери підхопили зміни
cr0x@server:~$ sudo systemctl reload nginx
cr0x@server:~$ systemctl status nginx --no-pager | sed -n '1,12p'
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Tue 2025-12-30 09:55:01 UTC; 18min ago
Docs: man:nginx(8)
Що це означає: Перезавантаження пройшло успішно і Nginx лишається активним.
Рішення: Якщо перезавантаження не вдалось, припиніть і виправте. Налаштування лімітерів у зламаному deployment-пайплайні — просто мистецтво продуктивності.
Швидкий план діагностики
Коли різко зростають 429 або користувачі скаржаться, не блукайте. Виконуйте це по порядку. Мета — знайти вузьке місце (політика, ключ або ємність) менше ніж за 10 хвилин.
Перш за все: підтвердити, хто генерує 429 і чому
- Перевірте access логи на статус 429 і топ-шляхи (Завдання 12, Завдання 13).
- Перевірте error.log на повідомлення
limiting requests(Завдання 6).- Якщо в error.log є відхилення через лімітер: це політика Nginx.
- Якщо ні: 429 може генерувати upstream додаток або рівень шлюза.
- Перевірте заголовки через curl (Завдання 8), щоб бачити, чи Nginx повертає 429.
По-друге: перевірити, чи ключ адекватний у вашій топології
- Подивіться в логовані IP клієнтів (Завдання 4).
- Якщо ви бачите IP балансувальника: конфіг real IP відсутній/поламан.
- Якщо ви бачите невелику кількість NAT IP: лімітування за IP може карати багато користувачів.
- Перевірте межі довіри Real IP (Завдання 5). Переконайтесь, що ви довіряєте лише своїм проксі.
По-третє: вирішити, чи пороги або burst невірні (або чи потрібні специфічні ліміти для ендпоінтів)
- Перегляньте поточні зони і швидкості (Завдання 14).
- Перевірте використання HTTP/2 (Завдання 11). Якщо включено, збільшіть burst для фронтенд-ендпоінтів.
- Перевірте, чи проблема в зʼєднаннях чи запитах (Завдання 10). Для «захопників» зʼєднань використовуйте
limit_conn.
По-четверте: переконатися, що це не проблема ємності, яка маскується під лімітування
Коли upstream сповільнюється, повтори зростають, що збільшує швидкість запитів і тригерить ваш лімитер. Лімитер не «неправильний»; він виявляє реальну проблему ємності.
Використайте наявні метрики, але на середині коробки ви хоча б можете логувати поля часу upstream і помітити повільні upstream. Якщо upstream часу високі, налаштуйте upstream або додайте кешування, потім перегляньте лімітер.
Три корпоративні міні-історії з практики
Міні-історія 1: Інцидент через хибне припущення (NAT — не «рідкість»)
Середня B2B SaaS компанія впровадила Nginx limit_req з привабливим правилом: 5 запитів на секунду за IP для /api/. В стагінгу це здавалося ідеальним. У проді все було добре, доки не настала понеділкова ранкова хвиля в Північній Америці.
Квитки в підтримку посипались хвилями: «Додаток зависає», «Дашборд не завантажується», «Випадкові 429». Інженери дивились на дашборди і бачили незначне зростання трафіку — нічого драматичного. Командир інциденту спочатку підозрював поганий деплой. Відкат не допоміг.
На крайовому вузлі access логи розповіли історію: кілька платних клієнтів виглядали з одних і тих самих IP. Це не були «користувачі». Це були корпоративні шлюзи виходу та пул NAT операторів. Один офіс зі сотнями співробітників, що ділили один вихідний IP, тепер разом «витрачав» лише 5 запитів на секунду на весь додаток.
Термінове виправлення було грубим, але ефективним: значно підняли ліміт за IP і додали суворіші правила лише для чутливих ендпоінтів (вхід, скидання пароля). Справжнє виправлення — кращий вибір ключів: per API token для автентифікованих API і per session cookie для браузерного трафіку, де можливо.
Після подій вони додали у чекліст перед запуском: «Показати розподіл унікальних IP проти автентифікованих ID під час піку». Це стало рутинною частиною планування ємності, а не післядумкою.
Міні-історія 2: Оптимізація, що дала зворотний ефект (nodelay усюди)
Інша компанія мала проблему з шумними клієнтами. Хтось запропонував: «Використайте burst з nodelay, щоб реальні користувачі не відчували уповільнення». Вони застосували це широко: всі API-ендпоінти, усі клієнти, щедрий burst, nodelay увімкнено.
Користувацький досвід покращився у щасливому сценарії. Потім партнер по інтеграції неправильно налаштував політику повторних спроб. Їхній клієнт переживав таймаути і агресивно повторював з паралелізмом. З nodelay і великим burst Nginx охоче пропускав великі мікросплески прямо до upstream-сервісів, які вже боролись.
Upstream-сервіси відвалились, таймаути зросли, що підвищило повторы, що ще більше збільшило сплески. Петля з підсиленням під приємною користувацькою оболонкою. Моніторинг показав, що Nginx «в порядку» і CPU стабільний, але помилки upstream і затримки злетіли вгору.
Кінцеве рішення — більш нюансована політика: зберегти nodelay для кількох read-only ендпоінтів, що потребують швидкої відповіді, але прибрати nodelay для дорогих ендпоінтів і зменшити розміри burst. Також додали захист черги upstream і кращі таймаути/повтори в SDK клієнта.
Мораль не у тому, що «nodelay поганий». Мораль в тому, що згладжування і UX мають узгоджуватись з потужністю бекенду, а не з бажаннями.
Міні-історія 3: Скупе, але правильне правило, що врятувало день (логування статусу лімітеру)
Команда фінтеху мала звичку, що виглядала нудно в код-ревʼю: кожного разу, коли додавали лімітер, вони додавали поля логів для $request_id, $limit_req_status і ключа, що використовувався (зашифрованого за потреби). Без винятків.
Одного вечора вони побачили сплеск 429 і падіння конверсії. Початкова підозра — атака ботів. Нею вона не була. Логи показали, що більшість 429 були «PASSED», а upstream повертав 429 — тобто додаток сам лімітував, бо зовнішній платіжний провайдер повертав помилки і їхній запобіжник відключався.
Оскільки логи на краю були ясні, вони не витратили годину на послаблення Nginx-лімітів і не запросили реальних зловмисників. Вони виправили логіку відмови провайдера, підкоригували вікна повторів і залишили політику на краю без змін.
Цей інцидент не переріс у багатокомандну сварку. Це було 40-хвилинне виправлення з чіткою хронологією. Нудне логування перемогло драматичні здогади.
Поширені помилки: симптом → корінь → виправлення
1) «Усі користувачі одночасно отримують 429»
Симптоми: раптові масові 429; користувачі по регіонах постраждали; логи показують один і той самий клієнтський IP.
Корінь: ви лімітуєте IP балансувальника/проксі, бо Real IP не налаштований (або налаштований неправильно).
Виправлення: налаштуйте real_ip_header і звузьте set_real_ip_from до довірених IP-діапазонів проксі. Потім використовуйте $binary_remote_addr (після обробки real IP) як ключ.
2) «Корпоративні клієнти скаржаться, домашні користувачі в порядку»
Симптоми: помилки в офісних мережах; мобільні і домашні користувачі без проблем; топ IP показує кілька IP з великим трафіком.
Корінь: ліміти за IP карають NAT-овані мережі та виходи VPN.
Виправлення: для автентифікованих ендпоінтів лімітуйте за API-ключем або сесією. Для анонімних — підвищте ліміти за IP і звужуйте суворі правила до конкретних дорогих ендпоінтів.
3) «Логін працює, але інколи ламається»
Симптоми: періодичні неуспішні входи; користувачі успішно заходять після очікування; сплески під час маркетингових кампаній.
Корінь: логін-флоу робить кілька викликів (отримання CSRF, MFA-перевірки, телеметрія) і тригерить суворий ліміт з занадто малим burst.
Виправлення: застосуйте ліміти до POST-ендпоінту з обліковими даними, а не до всього під /login. Збільшіть burst трохи; розгляньте затримку (без nodelay) замість відкидання при малій перевищеності.
4) «Ми ввімкнули лімітування, але атаки все ще шкодять»
Симптоми: навантаження на бекенд все ще високе; логи лімітерів показують низький відсоток відхилень; багато джерел трафіку.
Корінь: розподілений трафік (ботнети, резидентні проксі) робить обмеження за IP марними; або зона занадто мала і трешиться.
Виправлення: додайте ліміти за ідентичністю (API-ключ, токен), політики по ендпоінтах, кешування і захист upstream. Збільшіть розмір зон і моніторте ефект вилучення.
5) «Після налаштування upstream почав частіше падати»
Симптоми: менше 429, але більше 5xx від upstream; росте латентність.
Корінь: ви послабили ліміти без додаткової ємності; або ввімкнули nodelay з великим burst і пропустили сплески до upstream.
Виправлення: поверніть згладжування (прибравши nodelay) для дорогих ендпоінтів; встановіть розумні burst; налаштуйте таймаути upstream; додайте кешування і черги.
6) «Обхід лімітування»
Симптоми: лімітер здається неефективним; атакувальники зʼявляються під різними IP; логи показують дивні значення XFF.
Корінь: довіра до X-Forwarded-For від ненадійних джерел (set_real_ip_from 0.0.0.0/0 або еквівалент).
Виправлення: довіряйте лише відомим проксі; розгляньте PROXY protocol, якщо підтримується; перевірте, що клієнтський IP змінюється лише коли запити приходять з довірених проксі-мереж.
Контрольні списки / покроковий план
Це послідовність, що зазвичай працює в реальних середовищах без перетворення вашого краю на ігровий автомат.
Покроковий план: впровадити лімітування, не блокуючи людей
- Виправте ідентичність на краю.
- За проксі? Налаштуйте Real IP з вузькою довірою.
- Визначте, що для вас «клієнт»: IP для анонімних, токен для автентифікованих.
- Класифікуйте ендпоінти.
- Ендпоінти автентифікації (вхід, скидання) = суворо
- Дорогі запити (пошук, звіти) = доволі суворо
- Дешеві read-запити = помірно
- Статичні ресурси = зазвичай без обмежень
- Створіть окремі зони для кожного класу.
- Не використовуйте одну зону для всього. Ви не побачите, яка поведінка зловмисна.
- Встановіть початкові швидкості з ухилом на менше хибних спрацьовувань.
- Почніть вище, ніж думаєте, потім поступово знижуйте на основі спостережень.
- Використовуйте burst, щоб захистити людей від «рваного» трафіку браузерів.
- Логуйте статус лімітеру і request ID.
- Якщо не логуєте — не зможете налаштувати.
- Розгортайте поступово.
- Застосуйте до одного класу ендпоінтів спочатку (логін — хороший кандидат).
- Слідкуйте за кількістю 429, конверсіями та логами помилок.
- Визначте поведінку у відповіді.
- Використовуйте 429 для чесного троттлінгу.
- Розгляньте затримки (без
nodelay) для брутфорс-стримінгу, щоб витрачати час зловмисника.
- Тестуйте з реалістичною формою трафіку.
- Браузери (HTTP/2) і мобільні додатки поводяться «рвано».
- Включайте повтори в навантажувальні тести — у проді вони будуть.
Солідна базова конфігурація (думка автора)
Цей приклад припускає:
- Ви стоїте за довіреним LB в
10.10.0.0/16 - Ви хочете суворий захист логіну за IP
- Ви хочете захист API за API-ключем, якщо він є, інакше за IP
- Ви хочете логи, що розкажуть вам, що сталося
cr0x@server:~$ sudo sed -n '1,220p' /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main_ext '$remote_addr $request_id $time_local '
'"$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'lrs=$limit_req_status '
'xff="$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main_ext;
error_log /var/log/nginx/error.log warn;
real_ip_header X-Forwarded-For;
set_real_ip_from 10.10.0.0/16;
real_ip_recursive on;
# Key selection for APIs: API key if present, else IP.
map $http_x_api_key $api_limit_key {
default $http_x_api_key;
"" $binary_remote_addr;
}
# Whitelist internal monitoring and office VPN (example).
map $binary_remote_addr $is_whitelisted {
default 0;
192.0.2.44 1;
198.51.100.77 1;
}
# Zones: size according to expected unique keys.
limit_req_zone $binary_remote_addr zone=login_per_ip:20m rate=5r/m;
limit_req_zone $api_limit_key zone=api_per_key:100m rate=20r/s;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
cr0x@server:~$ sudo sed -n '1,220p' /etc/nginx/sites-enabled/example.conf
server {
listen 443 ssl http2;
server_name example;
# Default: do not rate limit everything. Target specific locations.
location = /login {
if ($is_whitelisted) { break; }
limit_req zone=login_per_ip burst=6;
proxy_pass http://app_upstream;
}
location = /password/reset {
if ($is_whitelisted) { break; }
limit_req zone=login_per_ip burst=3;
proxy_pass http://app_upstream;
}
location ^~ /api/ {
if ($is_whitelisted) { break; }
limit_req zone=api_per_key burst=40 nodelay;
proxy_pass http://app_upstream;
}
location / {
proxy_pass http://app_upstream;
}
}
Чому ця базова конфігурація працює:
- Вхід обмежується за IP у людському масштабі (за хвилину), дозволяючи невеликі сплески.
- API обмежується за ключем клієнта, коли він доступний, що уникатиме проблем NAT.
- Лімітування таргетоване; ви не ламаєте завантаження ресурсів чи базову навігацію.
- Логи фіксують статус лімітеру, тож ви можете налаштовувати на підставі фактів.
Жарт 2/2: Якщо ви лімітуєте власні health checks, вітаю — ви винайшли «самодогляд» для серверів, і вони сприймуть це буквально.
Поширені запитання
1) У чому різниця між limit_req і limit_conn?
limit_req обмежує швидкість запитів (r/s або r/m). limit_conn обмежує одночасні зʼєднання. Використовуйте ліміти запитів для хвиль і чесності, а ліміти зʼєднань — для утримувачів зʼєднань і довготривалих стрімів.
2) Чи варто лімітувати глобально на location /?
Практично ніколи. Ви накажете нормальну поведінку браузера і повторів. Лімітуйте дорогі або чутливі ендпоінти, і тільки при реальній потребі додавайте ніжний baseline.
3) Чому реальні користувачі блокуються, а боти ні?
Бо ваш ключ неправильний (NAT/проксі), ваші пороги замалі для нормальних сплесків, або боти розподілені по багатьох IP, тоді як ваші користувачі зосереджені за спільними вихідними IP.
4) Чи краще $binary_remote_addr ніж $remote_addr?
Для ключів лімітеру — так. Це компактне бінарне представлення і рекомендується для ефективності в спільній пам’яті. Але воно допоможе лише якщо Real IP правильно налаштовано за проксі.
5) Коли варто використати nodelay?
Використовуйте його, коли хочете дозволити короткі сплески без додаткової латентності і коли upstream може витримувати короткі пики. Уникайте на дорогих ендпоінтах або коли upstream уже нестабільний.
6) Як уникнути покарання користувачів за NAT?
Лімітуйте автентифікований трафік за ідентичністю клієнта (API-ключ, токен, сесія) і тримайте пер-IP ліміти переважно для анонімних чутливих ендпоінтів. Також уникайте крихітних per-IP порогів для загальних API.
7) Чи можна безпечно «вбілювати» деяких клієнтів?
Так, але тримайте список вузьким і підзвітним (IP моніторингу, egress VPN офісів, конкретні IP партнерів). White-listи схильні розростатись і ставати ризиком, якщо їх не контролювати.
8) Як дізнатись, що зона лімітеру занадто мала?
Ви побачите непослідовне лімітування при великій кількості унікальних ключів і шаблони, що не мають сенсу. Практично: якщо у вас публічний ендпоінт і маленька зона, збільшіть її і подивіться, чи стабілізується поведінка лімітеру.
9) Чи завжди правильно повертати 429?
Для чесного троттлінгу — так. Для стримування брутфорсу краще затримувати (без nodelay). Для захисту від повного перевантаження також може мати сенс 503 з семантикою відступу, але робіть це свідомо.
10) Чи може Nginx робити лімітування по імені користувача при вході?
Не чисто зі стандартними змінними, бо імʼя зазвичай в тілі POST. Робіть per-IP на рівні Nginx, а per-username і контролі по акаунту — на рівні додатка.
Висновок: практичні наступні кроки
Якщо ви хочете обмеження швидкості, що не вбиває реальних користувачів, зробіть три речі перед тим, як міняти бодай одне число:
- Виправте ідентичність на краю. Реальний IP за проксі і ідентифікація для автентифікованих API.
- Лімітуйте за поведінкою, а не за сайтом. Розділіть зони і політики для login/reset/search/API. Решту залишайте у спокої, якщо немає підстав.
- Зробіть 429 діагностованими. Логуйте
$limit_req_status, додавайте request ID і перевіряйте error.log на повідомлення лімітерів.
Потім налаштовуйте як доросла людина: вимірюйте, змінюйте одну річ, безпечно перезавантажуйте і спостерігайте результати. Ваша мета не «менше запитів». Ваша мета — «менше поганих запитів, ті ж самі хороші користувачі». Ось і все завдання.