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

Почему стриминг через Nginx часто перестаёт быть стримингом
Приложение отправляет клиенту данные по одной строке, но браузер получает их пачкой через несколько секунд. Частая причина — буферизация в Nginx: прокси ждёт, пока накопится достаточно ответа, и только потом пересылает его дальше. На локальном стенде это легко не заметить, а в продакшене задержка превращает SSE-уведомления, потоковую генерацию и длинные ответы в обычный медленный запрос.
Ниже — практическая схема для Nginx Open Source 1.25 и новее, когда Nginx проксирует HTTP-ответ от приложения. Главная задача — отключить буфер именно для потокового маршрута, не ломая буферизацию для всего сайта. WebSocket здесь не разбирается: у него другой режим работы и отдельная настройка proxy_pass с заголовками Upgrade.
Для стриминга важна не только скорость приложения. Критично, когда каждый фрагмент ответа покидает сервер и доходит до клиента.
Сначала определите, что именно вы стримите
Одинаковая настройка не подходит для всех сценариев. SSE держит HTTP-соединение открытым и передаёт события в формате text/event-stream. Потоковая генерация текста обычно использует тот же принцип, хотя формат может быть JSON-строками или фрагментами текста. Загрузка большого файла — другой случай: там важнее размер буфера, поддержка диапазонов и отсутствие лишнего чтения файла в память.
| Сценарий | Что важно в Nginx | Главный риск |
|---|---|---|
| SSE и события | proxy_buffering off, достаточный proxy_read_timeout | Клиент получает события пачками или соединение закрывается по тайм-ауту |
| Потоковая генерация ответа | Отключить буферизацию на маршруте, проверить сжатие и промежуточные прокси | Первый фрагмент приходит только после завершения части ответа |
| Большой файл | Настройки раздачи файлов, диапазоны и лимиты на размер ответа | Файл начинает проксироваться через приложение вместо эффективной раздачи |
| WebSocket | Отдельные заголовки Upgrade и Connection | Попытка применить SSE-конфигурацию к двунаправленному протоколу |
Потоковая выдача — способ отправлять тело HTTP-ответа частями, не дожидаясь, пока приложение сформирует весь ответ целиком.
Базовая конфигурация для SSE и HTTP-стрима
Разумнее вынести поток в отдельный URL, например /api/events или /api/generate. Тогда отключённая буферизация не повлияет на обычные страницы, API и статические файлы.
location /api/events {
proxy_pass http://application;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
add_header X-Accel-Buffering no;
}Эта конфигурация написана для Nginx Open Source 1.25+. Имя upstream application должно существовать в блоке http или быть заменено на адрес вашего приложения. После изменения проверьте конфигурацию командой nginx -t, затем перечитайте конфигурацию штатным способом вашей системы.
proxy_buffering off запрещает Nginx накапливать ответ от upstream перед отправкой клиенту. proxy_cache off исключает кеширование длинного и персонального потока. proxy_read_timeout задаёт максимальную паузу между чтением данных от upstream, а не общий срок жизни соединения. Если приложение отправляет heartbeat чаще тайм-аута, соединение может жить дольше указанного значения.
Заголовок от приложения и настройка Nginx
Для SSE приложение должно вернуть как минимум корректный тип содержимого, обычно Content-Type: text/event-stream. Полезно также отправлять периодические комментарии или heartbeat, чтобы промежуточные устройства видели активность соединения. Заголовок X-Accel-Buffering: no приложение может передать само; в конфигурации Nginx его иногда добавляют на маршруте как дополнительную страховку.
Не добавляйте один и тот же заголовок вслепую и на стороне приложения, и на стороне прокси. Сначала посмотрите фактический ответ через инструменты разработчика или curl. Для закрытого внутреннего теста можно проверить поток так:
curl -N https://example.com/api/eventsКлюч -N отключает буферизацию вывода у самого curl. Без него клиент может скрыть проблему, которая на самом деле находится не в Nginx, а в тестовом инструменте.
Три подхода и цена каждого
Отключить буферизацию на нужном location
Это основной вариант для SSE и ответов, которые должны приходить частями. Он сохраняет обычную буферизацию для остальных маршрутов и локализует последствия. Цена — больше мелких операций записи, соединения живут дольше, а каждый подключённый клиент занимает ресурсы Nginx и приложения.
Отключить буферизацию для всего reverse proxy
Так делать проще, но это плохой универсальный пресет. Обычные API-ответы и страницы могут начать передаваться менее эффективно, а поведение станет одинаковым для маршрутов с совершенно разными требованиями. Такой вариант оправдан только после измерений и при понятной архитектуре, где весь трафик действительно потоковый.
Оставить буферизацию и увеличить тайм-аут
Это решает только преждевременное закрытие долгого запроса. Данные по-прежнему могут приходить пачками, поэтому для SSE и интерактивной генерации подход не подходит. Увеличенный тайм-аут без heartbeat также не гарантирует стабильное соединение: промежуточный балансировщик или CDN может иметь собственный лимит.
| Подход | Когда выбирать | Чем платите |
|---|---|---|
| Буферизация выключена для одного location | SSE, токены генерации, длинный HTTP-ответ | Долгоживущие соединения и больше операций записи |
| Буферизация выключена везде | Редкая специализированная архитектура | Риск ухудшить обычный трафик |
| Буферизация оставлена, тайм-аут увеличен | Долгий, но не потоковый ответ | Нет гарантии немедленной доставки фрагментов |
Что чаще всего ломается
Данные приходят только после завершения запроса
Проверьте три уровня: приложение, Nginx и внешний прокси. В Nginx ищите наследование настроек proxy_buffering, включённый кеш и другой location, который фактически обрабатывает URL. В приложении проверьте, что сервер действительно делает flush данных, а фреймворк не собирает весь ответ в память. Если запрос проходит через CDN или ingress, у него могут быть собственные правила буферизации.
Соединение закрывается примерно через одинаковый интервал
Сравните значение proxy_read_timeout с частотой heartbeat. Этот тайм-аут срабатывает, когда Nginx не получает байты от upstream. Если heartbeat есть, но соединение всё равно закрывается, ищите ограничение на следующем прокси, балансировщике или стороне клиента. Увеличивать тайм-аут бесконечно не стоит: сначала определите, какой компонент завершает соединение.
После включения gzip задержка стала заметнее
Сжатие полезно для обычного текста, но для очень маленьких частых фрагментов может мешать немедленной доставке или давать небольшой выигрыш. Проверьте фактическое поведение именно вашего стека: Nginx, приложение, браузер и внешний прокси. Для критичного SSE-маршрута задайте отдельные правила сжатия только после замера, а не копируйте глобальную настройку с сайта.
Работает через curl, но не работает в браузере
Проверьте формат SSE, CORS, авторизацию и обработку переподключения на клиенте. Nginx отвечает за проксирование, но не исправляет неверное поле события и не добавляет логику повторного подключения. Если используется HTTP/2, отдельно проверьте реальное поведение браузера и всех промежуточных прокси: наличие HTTP/2 само по себе не отменяет требования к корректному потоку.
Память Nginx растёт при большом числе клиентов
Долгие соединения не бесплатны. Посчитайте число одновременных клиентов, частоту heartbeat и размер очереди данных в приложении. Следите за активными соединениями, временем ответа, кодами 499 и 5xx, нагрузкой worker-процессов и количеством открытых файлов. Если один клиент читает медленно, приложение и прокси могут дольше удерживать данные в очереди.
Что проверять на продакшене
Одиночный тест показывает только факт доставки. Для практической проверки нужны сценарии с несколькими клиентами и разной скоростью чтения. Замеряйте время от отправки фрагмента приложением до его появления у клиента, а не только полное время ответа.
- Сравните поток напрямую с портом приложения и через публичный адрес Nginx.
- Проверьте быстрый клиент, медленный клиент и разрыв соединения посередине ответа.
- Убедитесь, что обычные API-маршруты сохранили прежнее кеширование и время ответа.
- Посмотрите, не закрывает ли соединения внешний балансировщик раньше Nginx.
- Проверьте повторное подключение SSE и отсутствие дублирования событий после сетевого сбоя.
Для диагностики полезно временно добавить в access log время ответа и статус запроса, а затем сопоставить их с логами приложения. Важен не только код 200: запрос, который формально завершился успешно, мог доставить клиенту данные с большой задержкой.
Когда эта схема не подходит
Если вам нужно раздать большой статический файл, сначала рассмотрите прямую раздачу Nginx, а не проксирование через приложение. Если нужен двусторонний обмен сообщениями с отдельными кадрами и быстрым подтверждением, смотрите в сторону WebSocket или другого подходящего протокола. Если поток проходит через CDN, изучите его правила для кеширования, тайм-аутов и передачи событий: одна конфигурация Nginx не контролирует весь маршрут.
Инвалидация кешей и выбор протокола для интерактивных приложений — отдельные темы. Здесь важна только доставка HTTP-ответа частями.
Что делать дальше
Начните с отдельного location для потокового endpoint и выключите буферизацию только там. Затем проверьте поток через Nginx с помощью клиента, который не скрывает задержки, и сравните время доставки каждого фрагмента с прямым запросом к приложению. После этого нагрузите сценарий медленными клиентами и настройте heartbeat, тайм-ауты и лимиты по фактическим метрикам, а не по максимальным значениям из примеров.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: NGINX documentation: ngx_http_proxy_module, NGINX documentation: ngx_http_core_module
Частые вопросы
Нужно ли всегда ставить proxy_buffering off для SSE?
Почему proxy_read_timeout не ограничивает соединение ровно одним часом?
Можно ли отключить буферизацию во всём Nginx?
Почему curl показывает поток, а браузер получает данные пачками?
Подойдёт ли эта конфигурация для WebSocket?
Что делать, если после настройки соединения всё равно закрываются?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.