Если ваш бэкенд (PHP‑FPM, Node.js, Python, Java) периодически «залипает» под всплесками входящих запросов, а в логах видны волны 5xx и резкие скачки латентности, это классическая нехватка управляемого давления на уровень concurrency. У Nginx есть малозаметный, но крайне полезный инструмент — очередь к upstream через директиву queue. Она позволяет аккуратно дозировать одновременную нагрузку на серверы в группе и удерживать лишние запросы в короткой очереди, возвращая предсказуемые коды ошибок, если ждать уже нельзя.
Что такое backpressure на практике
Backpressure — это управляемое противодавление, которое вы создаёте на границе (в реверс‑прокси), чтобы бэкенд работал в своём комфортном окне параллелизма. Вместо того чтобы бездумно пропускать всё подряд в приложение (где начнутся блокировки, своп и таймауты), Nginx ограничивает одновременные соединения и, при необходимости, ставит новые запросы в очередь на короткое время.
Цель: ограничить одновременную обработку (concurrency) и дать краткосрочной очереди сгладить bursts, не превращая их в лавину ретраев и 5xx.
Как работает директива queue в upstream
Директива queue задаётся в контексте upstream и определяет максимальный размер очереди и время ожидания свободного соединения к бэкенду. Используется совместно с zone (общая память для состояния upstream между рабочими процессами) и часто — с ограничением одновременных подключений max_conns на серверах.
Логика простая:
- Пока доступно свободное «место» по
max_connsв группе — запросы немедленно идут в бэкенд. - Если все заняты, новые запросы помещаются в общую очередь (
queue N), где ждут освобождения. - Если очередь переполнена — клиент получает немедленный отказ (обычно 503).
- Если запрос простаивает в очереди дольше
timeout— возвращается таймаут (обычно 504).
Ключевой плюс: вы заранее определяете границы — сколько параллельных запросов выдерживает приложение и сколько миллисекунд новая волна может постоять, прежде чем вы честно скажете «приходите позже».
Базовая конфигурация: минимальный рабочий пример
Ниже пример для HTTP‑бэкенда (например, Gunicorn/Node.js) на localhost:8080. Мы ограничиваем одновременные соединения через max_conns, включаем общий zone, активируем очередь queue и задаём небольшой timeout.
upstream app {
zone app_zone 128k;
least_conn;
server 127.0.0.1:8080 max_conns=200;
keepalive 100;
queue 500 timeout=2s;
}
server {
listen 80;
access_log /var/log/nginx/app.access.log main;
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 1s;
proxy_read_timeout 15s;
proxy_send_timeout 15s;
proxy_buffering on;
}
error_page 503 /busy.html;
error_page 504 /busy.html;
location = /busy.html {
internal;
add_header Cache-Control no-store;
return 200 "Please try again in a moment";
}
}
Суть:
zoneобязателен для корректной очереди и учётаmax_connsмежду всеми воркерами.max_connsзадаёт «ширину горлышка»: ровно столько одновременных запросов пропускаем к приложению.queueиtimeoutформируют короткую буферную зону для всплесков.error_pageдаёт контролируемый ответ при отказе (вместо дефолтной ошибки).

PHP‑FPM/FCGI: что учесть
С FastCGI логика та же, но важно не забыть про реюз соединений к пулу и лимиты пула. В примере ниже pm.max_children в PHP‑FPM определяет верхнюю границу параллельно работающих воркеров; её и используем для max_conns.
upstream php_fpm {
zone php_zone 128k;
server unix:/run/php/php-fpm.sock max_conns=50;
queue 200 timeout=1.5s;
}
server {
listen 80;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass php_fpm;
fastcgi_keep_conn on;
fastcgi_connect_timeout 1s;
fastcgi_read_timeout 15s;
fastcgi_send_timeout 15s;
fastcgi_buffers 32 32k;
}
}
Выставляя max_conns, ориентируйтесь на pm.max_children и реальные ресурсы CPU/памяти. Для PHP‑проектов дополнительно имеет смысл оптимизировать кэш кода и компрессию — примерный чек‑лист есть в материале про оптимизацию PHP и OPcache.
Расчёт параметров: откуда взять цифры
1) max_conns
Это ваш «конвейер». Возьмите число реальных воркеров приложения, умножьте на коэффициент 1.0–1.2 (при I/O‑bound нагрузке можно 1.5), но не больше того, что выдержит CPU/память без деградации. Если в пуле PHP‑FPM 50 детей, в Gunicorn 8 воркеров, в Node.js кластер на 4 процесса — берите эти числа и не пытайтесь «ускорить» систему расширением окна параллелизма в прокси. Оценивайте пределы машины, на которой крутится приложение, например вашего VDS.
2) queue
Очередь должна сглаживать короткие bursts, а не превращаться в «второй дата‑центр». Подход: возьмите пиковый входящий rps и хвост распределения времени обработки (p95/p99), оцените, сколько запросов за 1–2 секунды может «накопиться». Именно это число и задайте. Для средней витрины это часто 200–1000; для тяжёлых API — 50–200. Делайте замеры и не злоупотребляйте размерами.
3) timeout
Порог терпения пользователя короткий, а ретраи клиентов часто агрессивны. Лучше короткий таймаут очереди: 500 мс–2 с для веб‑страниц, 100–500 мс для низколатентных API. Чем больше timeout — тем выше риск «складирования» запросов с лавинообразной деградацией.
Быстрый способ прикинуть
Пример: есть 150 rps на пике, средняя обработка 80–120 мс, 95‑перцентиль 280 мс, стабильные 200 одновременных коннектов к приложению запускают сборку мусора и спайки. Тогда:
max_conns= 150 (под CPU),queue= 300–400 (1–2 секунды накопления при bursts),timeout= 1–1.5 s.
Финально подтверждаем на нагрузочном тесте и мониторинге задержек.
queue vs limit_req burst: что где уместно
Частая путаница: «у нас же есть limit_req с burst, зачем queue?» Эти механизмы о разном:
limit_reqограничивает входящую частоту запросов (rps), может отбросить избыток или допустить краткосрочныйburstс задержкой.queueработает на уровне доступа к бэкенду и контролирует именно параллелизм (concurrency). Она вступает в игру, когда все соединения к upstream заняты.
Лучший результат — комбинация. limit_req обрежет паразитные всплески на входе, а queue не даст добраться до приложения слишком большому числу одновременных запросов, которые всё же прошли через лимитер.
Коды ошибок и поведение при отказе
Когда очередь переполнена, Nginx сразу возвращает отказ (обычно 503). Если запрос простоял в очереди дольше лимита timeout, клиент получает таймаут (часто 504). Оба сценария нужно осмысленно обрабатывать: отдавайте лёгкую «busy»‑страницу, JSON‑ответ с кодом состояния для API и ставьте корректные заголовки кэширования, чтобы посредники не кэшировали такие ответы.
Ещё пара советов:
- Не включайте «тяжёлые» обработчики на страницах с ошибками. Пусть это будет простой текст.
- Логируйте факт переполнения очереди отдельно, чтобы быстро замечать деградацию.
Логи и метрики: как увидеть очередь
Для анализа стоит расширить формат логов. В современных релизах можно логировать время ожидания в очереди, если соответствующая переменная поддерживается сборкой. В любом случае полезно видеть общий $request_time, время апстрима $upstream_response_time и статус апстрима $upstream_status.
log_format main '$remote_addr $request $status $body_bytes_sent '
'$request_time $upstream_status $upstream_response_time';
Если у вашей сборки есть переменная для времени очереди, добавьте её рядом. При отсутствии — косвенно контролируйте хвосты: рост $request_time без соответствующего роста $upstream_response_time зачастую указывает на задержку до апстрима (включая очередь).

Важные тонкости и взаимодействие директив
- Без
zoneочередь не работает как надо. Состояние занятости серверов и длина очереди должны быть общими для всех воркеров. Всегда объявляйтеzoneна upstream‑группе. max_connsобязателен концептуально. Если его не задать, очередь мало что ограничит: Nginx будет считать сервер «свободным» почти всегда. Привязывайте лимит к воркерам приложения.- Балансировка.
least_connхорошо сочетается с очередью для неоднородных запросов. Для кэшируемых страниц бывает достаточно round robin по умолчанию. - Повторные попытки. Не перебарщивайте с
proxy_next_upstreamи ретраями клиента: ретраи поверх очереди легко превращаются в шторм. - Буферизация. Включённая
proxy_bufferingразгружает апстрим от медленных клиентов — особенно важно при пиках. - Кэш. Если значимая доля запросов кэшируема, микрокэш на Nginx уберёт десятки процентов нагрузки ещё до очереди.
- HTTP/2 и очереди. Мультиплексирование не отменяет лимитов на апстрим. Очередь всё равно нужна.
Стратегия внедрения без сюрпризов
- Зафиксируйте текущие метрики: rps, p95/p99 времени ответа, одновременные подключения к приложению, ошибки 5xx.
- Определите безопасный
max_connsиз параметров пула/воркеров и CPU/памяти. - Поставьте консервативные
queueиtimeout(например, 200–500 и 1–2 s). - Разверните конфигурацию поэтапно (canary/blue‑green), проверьте логи.
- Покрутите параметры, добиваясь плоского профиля латентности и отсутствия очереди в спокойные периоды.
- Добавьте контролируемые «busy»‑страницы и оповещения при скачках 503/504.
Типичные ошибки
- Слишком длинная очередь. Клиенты ждут по 5–10 секунд — UX страдает, ретраи множатся, а бэкенд всё равно умирает.
- Отсутствие
max_conns. Очередь не ограничивает параллелизм без потолка на соединения к апстриму. - Нереалистичные таймауты апстрима.
proxy_read_timeoutи др. должны соотноситься со временем работы бэкенда иtimeoutв очереди. - Нет общего
zone. Воркеры видят картину по‑разному и очередь перестаёт быть глобальной. - Самообман «мы выдержим любой burst». Нагрузочные тесты быстро покажут обратное. Очередь — это про управляемый отказ, а не бесконечный буфер.
Связанные настройки, которые усиливают эффект
- Тюнинг keepalive к апстриму. Держите достаточно «тёплых» соединений (
keepaliveвupstreamи соответствующие настройки приложения), чтобы не тратить время на коннекты. - Разделение маршрутов. Вынесите тяжёлые API‑эндпоинты в отдельные
upstream‑группы с собственными лимитами и очередью. Для статики — другой блок, зачастую без очереди. - Заголовки кэширования. Правильные
Cache-Controlи микрокэш снижают нагрузку до того, как запрос доберётся до очереди.
Проверка в бою: как понять, что стало лучше
- Стабилизация p95/p99, исчезновение длинных хвостов в моменты пиков.
- Ровная загрузка CPU и памяти бэкенда, отсутствие «залипаний» воркеров.
- Редкие и предсказуемые 503/504 вместо случайной россыпи 502/499.
- Снижение количества клиентских ретраев и сужение «лавины».
FAQ
Нужна ли очередь, если у меня есть autoscaling приложения?
Да. Автоскейлинг реагирует с задержкой. Очередь сгладит секунды/десятки секунд, пока раскатывается новый инстанс, и защитит текущие.
Можно ли ставить очередь только на часть запросов?
Да. Вынесите нужные локации в отдельный upstream с собственной queue и max_conns. Для лёгких эндпоинтов очередь не нужна вовсе.
Повлияет ли очередь на WebSocket/стриминг?
Очередь относится к моменту установления соединения с апстримом. Долго живущие соединения вынесите в отдельную группу и лимитируйте отдельно, иначе они «съедят» весь конвейер.
Какой размер зоны zone выбирать?
Для десятков/сотен серверов в группе достаточно десятков/сотен килобайт. Начните со 128k и увеличивайте при необходимости — это служебная память для состояния.
Итог
Директива queue в upstream — простой способ ввести дисциплину параллелизма и дать системе управляемый backpressure. Вместе с max_conns, умеренными таймаутами и грамотной буферизацией она превращает случайные пики в предсказуемые события: часть запросов будет вежливо отвергнута быстро, остальные — обработаны в нормальном режиме, без лавинообразных провалов. Добавьте к этому кэш и базовые лимиты по rps — и ваш стек начнёт «дышать» ровно даже под пиковые нагрузки.


