Буферизация в Nginx: почему ломается стриминг
Nginx может принять поток от приложения, собрать его в буфере и отдать браузеру одним блоком. Разбираем, как отличить такую задержку от проблем приложения, какие настройки проверить и где отключать буферизацию.

Почему поток доходит одним большим ответом
Приложение отправляет данные каждые несколько секунд, но клиент видит их только в конце запроса. Частая причина — proxy_buffering: Nginx принимает ответ от upstream, складывает его в буферы и не спешит передавать маленькие фрагменты дальше. Для обычного HTML это экономит соединения и сглаживает нагрузку. Для SSE, длинных ответов и генерации текста по частям превращает поток в обычный delayed response.
Ниже — разбор для Nginx Open Source 1.25.x. Названия директив и базовое поведение актуальны для этой ветки; перед изменением конфигурации проверьте конкретную версию командой nginx -v. Задача не в том, чтобы выключить буферы на всём сервере, а в том, чтобы сделать потоковый endpoint исключением.
Если приложение «стримит», это ещё не означает, что клиент получит данные сразу: каждый слой цепочки может ждать следующую порцию.
Где именно возникает задержка
У потокового ответа обычно есть несколько участников: приложение, Nginx, балансировщик, CDN и браузер или другой клиент. Данные должны быть отправлены приложением, приняты Nginx и переданы клиенту без накопления. Если один слой буферизует ответ, внешне это выглядит одинаково: соединение открыто, но новых данных нет.
Буферизация ответа1 — накопление уже полученных от upstream данных перед отправкой клиенту. Не путайте её с proxy_request_buffering: последняя относится к телу запроса от клиента к приложению. Отключение request buffering не исправит задержку в SSE.
Что делает proxy_buffering
В режиме по умолчанию proxy_buffering on Nginx читает ответ upstream настолько быстро, насколько позволяет серверное приложение, и раскладывает данные по буферам. Если ответ не помещается в оперативные буферы, часть может быть записана во временный файл. Клиент при этом получает данные порциями, размер и момент отправки зависят от заполнения буферов и работы фильтров.
При proxy_buffering off Nginx передаёт ответ клиенту синхронно, по мере получения от upstream. Это подходящий режим для endpoint, который должен отдавать данные до завершения генерации. Но он не заставляет приложение отправлять данные: если upstream сам ждёт, Nginx не сможет создать поток из ничего.
Почему маленький фрагмент всё равно не виден
- Приложение не сбрасывает вывод. Оно формирует несколько событий в памяти и отправляет их одним ответом.
- Буферизация осталась на другом location. Настройка в одном блоке не действует автоматически на соседний путь.
- Включён другой модуль. Для FastCGI и uWSGI используются отдельные директивы, а не только
proxy_buffering. - Ответ сжимается. Gzip-фильтр может накапливать данные, прежде чем сформировать очередной сжатый блок. Для SSE сжатие обычно отключают.
- Таймаут выглядит как «зависание». Если между чтениями от upstream проходит больше времени, Nginx может закрыть соединение по
proxy_read_timeout. - Клиент сам буферизует тело. Проверка через библиотеку, лог или DevTools не всегда показывает байты в момент их прихода.
Минимальная конфигурация для SSE
Для Server-Sent Events конфигурацию стоит ограничить location, который действительно отдаёт события. Пример ниже рассчитан на приложение, доступное в Docker-сети или на том же сервере по адресу 127.0.0.1:8000. Путь /events/ замените на свой endpoint.
server {
listen 443 ssl;
server_name example.com;
location /events/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
proxy_cache off;
gzip off;
proxy_read_timeout 1h;
}
}
proxy_http_version 1.1 и очистка заголовка Connection помогают не превращать проксируемое соединение в нежелательно короткое. proxy_read_timeout задаёт максимальный промежуток между чтениями от upstream, а не общую продолжительность запроса. Значение в час — пример для долгого соединения, а не универсальная рекомендация: оно должно соответствовать ожидаемой длительности сессии.
После изменения проверьте синтаксис и перезагрузите конфигурацию:
sudo nginx -t
sudo nginx -s reload
Проверка завершилась сообщением syntax is ok и test is successful, а reload не вернул ошибку. Если Nginx работает в контейнере, выполняйте команды внутри контейнера или используйте штатный способ перезагрузки вашего образа.
Заголовок X-Accel-Buffering
Приложение может попросить Nginx не буферизовать конкретный ответ заголовком X-Accel-Buffering: no. Это удобно, если один и тот же location обслуживает обычные и потоковые ответы. Такой заголовок должен реально попасть в ответ upstream и не быть удалён другим прокси.
Для SSE приложение также обычно отправляет Content-Type: text/event-stream и завершает каждое событие пустой строкой. Например, после строки data: сообщение должен быть разделитель событий. Если формат нарушен, браузер может не вызвать обработчик даже при исправной передаче байтов.
Как найти слой, который задерживает данные
Не начинайте с бесконечного увеличения таймаутов. Сначала сравните поведение приложения напрямую и через Nginx, затем посмотрите заголовки и время появления отдельных событий.
- Проверьте upstream напрямую. Подключитесь к порту приложения с той же машины и используйте клиента, который не собирает тело ответа. Если данные приходят порциями напрямую, приложение, скорее всего, умеет стримить.
- Проверьте публичный адрес. Сравните его с прямым запросом. Поток пропал только после прокси — ищите
proxy_buffering, gzip, CDN и второй Nginx. - Посмотрите заголовки. Проверьте
Content-Type,Content-Encoding,Cache-Controlи наличиеX-Accel-Buffering. ЗаголовокContent-Encoding: gzip— повод отдельно проверить сжатие. - Проверьте реальную конфигурацию. Выполните
nginx -Tи найдите все location, которые могут совпасть с URL. Файл, который вы редактировали, не обязательно содержит итоговую конфигурацию после include. - Измерьте интервалы. Запишите время отправки события приложением, время записи в access log и время получения клиентом. Без этой тройки легко обвинить Nginx в задержке, которая возникла в очереди приложения.
Для ручной проверки подойдёт curl с отключением собственной буферизации вывода:
curl -N -i https://example.com/events/
Ключ -N нужен только для того, чтобы curl не накапливал вывод. Он не отключает буферизацию Nginx. Если в терминале события появляются с нужным интервалом, а в браузерном интерфейсе нет, ищите проблему в клиентском коде или промежуточном прокси.
Что менять в зависимости от способа подключения
| Цепочка | Главная настройка | Что проверить дополнительно | Цена решения |
|---|---|---|---|
| HTTP-прокси через proxy_pass | proxy_buffering off | proxy_read_timeout, gzip, CDN | Долгие соединения занимают ресурсы Nginx и upstream |
| FastCGI | fastcgi_buffering off | Потоковый вывод приложения и таймауты | Больше открытых соединений при медленных клиентах |
| uWSGI | uwsgi_buffering off | Настройки uWSGI и формат ответа | Отключение буфера действует только на выбранный location |
| Прокси плюс CDN | Настройки Nginx и CDN | Правила кеширования и поддержка SSE | CDN может иметь собственную политику доставки потоков |
Не копируйте fastcgi_buffering off в блок с proxy_pass: директивы относятся к разным модулям. Если приложение работает через PHP-FPM, например, проверять нужно FastCGI-цепочку. Для Node.js, Go, Python-приложения за HTTP upstream обычно используется proxy_buffering.
Где решение ломается
Буфер отключён, но данные приходят в конце
Проверьте, делает ли приложение flush после каждой порции. В разных языках и фреймворках это зависит от сервера, middleware и способа формирования ответа. Также уберите промежуточное сжатие и убедитесь, что перед Nginx нет балансировщика с собственной буферизацией.
Соединение закрывается через несколько минут
Сравните интервалы между событиями с proxy_read_timeout. Таймер сбрасывается при получении данных от upstream, поэтому комментарии или heartbeat-события могут быть нужны, если бизнес-данных долго нет. Ставить бесконечный таймаут опасно: зависший клиент будет дольше занимать ресурсы.
После отключения буфера выросла нагрузка
Это ожидаемая цена потокового режима. Nginx больше не может быстро вычитать ответ и обслужить медленного клиента отдельно от upstream: открытые соединения дольше удерживают ресурсы. Ограничьте настройку одним endpoint, добавьте лимиты на число подключений и мониторьте активные соединения, задержку upstream и ошибки 499/502/504.
В HTTP/2 не видно chunked
Это не обязательно ошибка. В HTTP/2 нет привычного заголовка Transfer-Encoding: chunked: данные передаются кадрами протокола. Проверять нужно не наличие chunked, а время появления событий у клиента и интервалы между ними.
Что делать дальше
Сначала зафиксируйте контракт потока: формат события, допустимый интервал без данных, максимальную длительность соединения и поведение при переподключении. Затем настройте буферизацию только для потокового location, отключите сжатие на нём и добавьте heartbeat, если приложение может долго молчать.
В продакшене смотрите не только на среднее время ответа. Нужны число активных потоков, доля 499 и 504, время до первого события, интервалы между событиями и нагрузка на upstream. Инвалидация кеша и масштабирование SSE — отдельные темы; здесь важна именно доставка уже сформированных данных через Nginx.
1 Upstream — сервер или приложение, к которому Nginx обращается за исходным ответом. Для Nginx это может быть HTTP-сервис, PHP-FPM через FastCGI или uWSGI-приложение.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: NGINX documentation: ngx_http_proxy_module, NGINX documentation: ngx_http_fastcgi_module
Частые вопросы
Достаточно ли добавить proxy_buffering off?
Чем proxy_buffering отличается от proxy_request_buffering?
Нужно ли отключать буферизацию во всём Nginx?
Почему поток закрывается, хотя proxy_read_timeout большой?
Что делать, если у меня HTTP/2 и нет Transfer-Encoding: chunked?
Можно ли использовать gzip для SSE?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.