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

Поток SSE может работать напрямую с приложением и зависать за Nginx: браузер не получает события, соединение обрывается через минуту, а в логах появляется 504 или 499. Причина обычно в буферизации ответа, тайм-ауте между событиями или неправильных заголовках. Ниже — практическая конфигурация для Nginx 1.24 и новее, схема проверки и критерии выбора настроек для SSE и обычного потокового HTTP-ответа.
Почему обычный proxy_pass ломает стрим
Обычный reverse proxy рассчитан на ответ, который можно принять целиком или передать крупными блоками. SSE работает иначе: сервер держит HTTP-соединение открытым и отправляет небольшие события по мере их появления. Клиенту важно получить каждый фрагмент сразу, а не после накопления очередного буфера.
У Nginx в этом сценарии есть три независимые точки риска:
- Буферизация ответа. Прокси может принять часть ответа от upstream и не отправить её браузеру немедленно.
- Тайм-аут чтения. Nginx закрывает соединение, если от upstream долго не приходят новые данные.
- Поведение приложения. Фреймворк или сервер приложений может сам буферизовать вывод и не сбрасывать его в сокет.
Поэтому директива proxy_buffering off решает только одну часть проблемы. Если приложение молчит дольше тайм-аута или не отправляет корректный SSE-поток, настройка Nginx не поможет.
Для SSE нужно настраивать не только скорость ответа, но и паузу между двумя соседними байтами от upstream.
Базовая конфигурация отдельного SSE-маршрута
Не отключайте буферизацию для всего сайта. Выделите потоковый endpoint в отдельный location. Так обычные HTML-страницы, JSON-ответы и загрузки файлов продолжат использовать стандартное поведение прокси.
location /events/ {
proxy_pass http://app_backend;
# Передаём поток клиенту без накопления ответа на прокси.
proxy_buffering off;
proxy_cache off;
# SSE обычно работает через постоянное HTTP/1.1-соединение до upstream.
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Это тайм-аут простоя между чтениями ответа upstream.
proxy_read_timeout 1h;
# Не сжимаем поток в этом location без отдельного тестирования.
gzip off;
}
Синтаксис актуален для ветки Nginx 1.24+; точные значения тайм-аутов подберите под частоту событий и политику инфраструктуры. Если upstream называется иначе, замените только имя app_backend: сам upstream должен быть объявлен в секции http или задан адресом в proxy_pass.
Что проверяет каждая настройка
| Настройка | Зачем нужна | Цена решения |
|---|---|---|
proxy_buffering off | Отправляет части ответа клиенту по мере чтения | Больше операций ввода-вывода и соединений, чем у буферизованного ответа |
proxy_read_timeout 1h | Разрешает долгую паузу между событиями | Зависшее соединение может дольше занимать ресурсы |
proxy_http_version 1.1 | Сохраняет постоянное соединение между Nginx и приложением | Upstream должен корректно работать с HTTP/1.1 |
gzip off | Убирает ещё один слой, который может задерживать мелкие фрагменты | Поток передаётся без сжатия |
Заголовки, формат и отправка событий
SSE — это не произвольный текстовый поток. Сервер должен отдавать тип text/event-stream, не кэшировать ответ и завершать каждое событие пустой строкой. Минимальный блок выглядит так:
Content-Type: text/event-stream
Cache-Control: no-cache
id: 42
event: update
data: {"status":"ready"}
Заголовки должен формировать upstream, потому что именно приложение знает, какой endpoint является потоковым. На стороне Nginx можно дополнительно проверить, что кэширование отключено для этого маршрута, но не подменяйте тип ответа глобально: обычный JSON под тем же сервером не становится SSE от одного заголовка.
После записи события приложение должно сбросить данные в соединение. Конкретная команда зависит от языка, фреймворка и сервера приложений, поэтому универсального фрагмента здесь нет. Проверяйте поток через браузер или curl: если событие видно только после закрытия соединения, проблема ещё находится до или внутри upstream.
Heartbeat важнее большого тайм-аута
Длинный proxy_read_timeout не заменяет служебные heartbeat-сообщения. Если бизнес-события появляются раз в десять минут, приложение может отправлять комментарий SSE, например : keep-alive, чаще этого интервала. Клиент не показывает такой комментарий как событие, но соединение остаётся активным для прокси и промежуточных сетевых устройств.
Интервал heartbeat выбирайте меньше минимального тайм-аута между клиентом, балансировщиком и Nginx. Если перед Nginx стоит облачный load balancer, его ограничение может быть ниже установленного в конфигурации Nginx.
Когда нужен другой подход
Одна конфигурация не одинаково хороша для всех потоков. Выбор определяется тем, сколько клиент ждёт первый байт и как часто приходит следующий.
| Сценарий | Что настроить | На что смотреть |
|---|---|---|
| SSE с редкими событиями | Отключить buffering, задать read timeout с запасом, добавить heartbeat | Время между heartbeat и число оборванных соединений |
| SSE с частыми событиями | Отключить buffering и кэш, проверить flush в приложении | Задержку от генерации события до получения клиентом |
| Потоковая выдача большого ответа | Проверить, нужен ли именно SSE; для бинарных данных выбрать другой протокол | Память upstream, скорость передачи и восстановление после обрыва |
| Обычный JSON API | Оставить стандартную буферизацию Nginx | Время полного ответа и размер payload |
Если клиенту нужна только выдача результата по частям и формат событий SSE не используется, не добавляйте SSE-заголовки ради самого стриминга. Для HTML, JSON и файлов могут быть уместны другие механизмы и настройки.
Что ломается в проде
События приходят пачкой
Сначала проверьте proxy_buffering off, затем заголовок X-Accel-Buffering от upstream и буферизацию в приложении. Если через прямое подключение к приложению события идут по одному, а через Nginx — пачкой, сравнивайте именно этот маршрут и промежуточные прокси.
Соединение закрывается через фиксированный интервал
Ищите самый короткий тайм-аут в цепочке: Nginx, ingress, балансировщик, CDN или сервер приложений. proxy_read_timeout считает время тишины между чтениями ответа, а не максимальную продолжительность соединения. Поэтому поток с heartbeat и поток без heartbeat ведут себя по-разному даже при одинаковом общем времени работы.
В логах Nginx появляется 499
Код 499 в журналах Nginx означает, что клиент закрыл соединение раньше ответа. Это не доказательство ошибки upstream: вкладка могла быть закрыта, мобильная сеть могла сменить маршрут, а фронтенд — отменить запрос. Сопоставьте время 499 с логами приложения и метриками сети.
После изменения конфигурации ничего не изменилось
Проверьте, что запрос попадает в нужный location, конфигурация успешно проверена и Nginx перечитал её. Также убедитесь, что перед ним нет другого reverse proxy с собственной буферизацией. Для диагностики сравните прямой адрес upstream, адрес Nginx и публичный адрес сайта.
Как проверять результат
Проверяйте не только HTTP-код 200. Важны время до первого события, интервалы между событиями и причина закрытия соединения. В тесте оставьте подключение открытым дольше обычного тайм-аута и искусственно создайте паузу между событиями.
- Откройте SSE endpoint через тестовый клиент и убедитесь, что заголовок ответа содержит
Content-Type: text/event-stream. - Отправьте событие и измерьте время от его создания в приложении до появления у клиента.
- Сделайте паузу дольше обычного интервала между бизнес-событиями и проверьте, поддерживает ли heartbeat соединение.
- Посмотрите access log Nginx после закрытия: сравните статус, длительность запроса и объём переданных данных с логами приложения.
В проде добавьте метрики открытых SSE-соединений, времени жизни, частоты реконнектов и задержки доставки события. Самая полезная метрика — не число подключённых клиентов, а доля соединений, которые закрылись неожиданно, и время от публикации события до получения его клиентом.
Что делать дальше
Сначала выделите SSE в отдельный маршрут и примените минимальный набор настроек: отключите буферизацию и кэш, задайте HTTP/1.1 до upstream, увеличьте тайм-аут простоя и добавьте heartbeat на стороне приложения. Затем проверьте всю цепочку от клиента до upstream, включая ingress и балансировщик. Инвалидация подписок, повторная доставка событий и хранение истории — отдельные архитектурные вопросы; здесь важно сначала добиться предсказуемой доставки живого потока.
Буферизация — накопление части ответа перед отправкой следующему участнику цепочки. Для SSE это критично: клиент может получить все накопленные события одновременно, хотя приложение отправляло их с интервалом.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: NGINX ngx_http_proxy_module documentation, MDN: Using server-sent events
Частые вопросы
Достаточно ли поставить proxy_buffering off для SSE?
Почему SSE обрывается ровно через минуту?
Нужно ли отключать gzip для всех ответов сайта?
Можно ли использовать SSE через HTTPS и HTTP/2?
Что делать, если события приходят пачками даже после настройки Nginx?
Как понять, что 499 связан с сетью, а не с приложением?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.