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

Если SSE-эндпоинт за Nginx отдаёт события только после закрытия соединения или внезапно получает 504 через 60 секунд, проблема обычно не в EventSource. Прокси буферизует ответ, сжимает его или считает долгую паузу ошибкой. Ниже — практическая схема для Nginx 1.24–1.27: какие директивы менять, чем они отличаются и как понять, где именно застрял поток.
Что должно быть готово до настройки
Примеры рассчитаны на Nginx 1.24–1.27 и типичную схему, где приложение слушает внутренний адрес 127.0.0.1:8000. Проверьте установленную версию командой nginx -v, а после изменения конфигурации — синтаксис командой nginx -t.
- Приложение должно возвращать
Content-Type: text/event-stream. - Каждое событие должно заканчиваться пустой строкой: после строк
data: ...нужен дополнительный перенос. - Приложение должно отправлять данные во внешний поток, а не только записывать их во внутренний буфер.
- Путь SSE нужно вынести в отдельный
location, чтобы не отключать буферизацию для обычных HTML-страниц и API.
Потоковая передача1 — это отправка ответа частями по мере готовности, без ожидания полного тела ответа.
Для SSE мало «долго держать HTTP-соединение». Nginx должен ещё и пропускать небольшие части ответа к клиенту сразу.
Базовый location для SSE
Минимальная конфигурация выглядит так:
location /events/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
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;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;
gzip off;
}
proxy_buffering off запрещает Nginx накапливать ответ прокси перед отправкой клиенту. proxy_read_timeout задаёт не максимальную длину SSE-сессии, а допустимое время между двумя чтениями ответа от upstream. Поэтому значение «1h» не гарантирует, что соединение проживёт час: приложение или другой прокси могут закрыть его раньше.
Пустое значение в proxy_set_header Connection "" убирает этот заголовок при передаче запроса upstream. Это не заменяет отключение буферизации, но убирает лишние ограничения для долгого HTTP/1.1-соединения.
После правки выполните:
sudo nginx -t
sudo systemctl reload nginx
Проверка успешна, если nginx -t сообщает, что синтаксис корректен, а reload завершается без ошибки. Перезапускать весь сервер для изменения конфигурации обычно не требуется.
Четыре настройки, которые часто путают
Буферизация ответа
Включённая буферизация особенно заметна, когда события маленькие: приложение отправляет несколько байт, а браузер видит их только после накопления блока или закрытия ответа. Отключайте её именно в SSE-location, а не глобально в http.
Некоторые приложения могут управлять поведением Nginx через заголовок X-Accel-Buffering: no. Это удобно, когда один и тот же прокси обслуживает разные потоковые маршруты. Но заголовок должен реально дойти от приложения, а политика инфраструктуры может его скрыть или переопределить. Явный proxy_buffering off в location проще проверять по конфигурации.
Таймаут чтения
По умолчанию Nginx ждёт новые данные от upstream ограниченное время. Если приложение отправляет события раз в несколько минут и не посылает heartbeat, прокси решит, что upstream завис. Увеличьте proxy_read_timeout либо отправляйте периодический комментарий SSE, например строку, начинающуюся с двоеточия, с пустой строкой после неё.
Heartbeat не должен создавать ложные бизнес-события. Его задача — поддерживать активность соединения и обнаруживать разрыв канала. Интервал выбирают с запасом относительно самого короткого таймаута в цепочке: Nginx, балансировщика, ingress и CDN.
Сжатие
Gzip полезен для обычных JSON-ответов, но для небольших и редких SSE-сообщений может добавлять буферизацию и задержку. На время диагностики отключите gzip в SSE-location. Если сжатие необходимо, проверяйте его на конкретной версии Nginx и клиентском маршруте, а также не позволяйте upstream без необходимости возвращать уже сжатый ответ.
location /events/ {
proxy_pass http://127.0.0.1:8000;
proxy_buffering off;
gzip off;
proxy_set_header Accept-Encoding "";
}
Последняя строка убирает запрос сжатого ответа к upstream. Она нужна не всегда: применяйте её, если приложение или промежуточный прокси возвращает сжатый поток и вы подозреваете задержку из-за фильтров.
Кэширование
SSE нельзя обслуживать как обычный кэшируемый GET-ответ. Если перед потоком есть proxy cache, клиент может получить старый заголовок или накопленный фрагмент вместо живого соединения. Для этого location явно укажите proxy_cache off, а приложение пусть возвращает подходящие заголовки, например Cache-Control: no-cache.
Подходы к отключению задержки
| Подход | Когда применять | Цена и риск |
|---|---|---|
proxy_buffering off | Основной SSE-маршрут под вашим контролем | Меньше задержка, но каждый клиент дольше занимает соединение и ресурсы Nginx |
X-Accel-Buffering: no | Потоковые ответы определяются приложением | Гибче, но нужно проверить, что заголовок не теряется в цепочке прокси |
| Heartbeat | Между полезными событиями бывают длинные паузы | Удерживает соединение, но создаёт дополнительный трафик и нагрузку на приложение |
| Отключение gzip | Небольшие события приходят с задержкой или пачками | Больше трафик; настройка должна быть локальной для SSE |
Это не взаимоисключающие варианты. На практике часто нужны одновременно отключённая буферизация, разумный таймаут и heartbeat. Если события приходят крупными блоками, причина может быть на стороне приложения, а не Nginx.
Что ломается чаще всего
Браузер получает события только через несколько секунд
Сначала проверьте proxy_buffering off и заголовок Content-Type. Затем выполните запрос с отключённым выводом в буфер у клиента:
curl -N -i https://example.com/events/
Ключ -N отключает буферизацию вывода в curl. Если curl получает события сразу, а браузер нет, ищите проблему в клиентском коде, service worker, CDN или корпоративном прокси. Если задержка видна и в curl, проверяйте приложение и Nginx по отдельности.
Соединение закрывается примерно через минуту
Сравните время разрыва с proxy_read_timeout и таймаутами внешнего балансировщика. Если приложение молчит дольше этого значения, добавьте heartbeat. Увеличение только Nginx-таймаута не поможет, если раньше соединение закрывает ingress или CDN.
В логах появляется 504 Gateway Time-out
Статус означает, что Nginx не дождался данных от upstream в рамках своего таймаута. Посмотрите error.log и сопоставьте время ошибки с моментом последнего события. Не маскируйте проблему значением в несколько часов, если backend может зависнуть: долгий таймаут увеличит число занятых соединений.
После reload старые подключения ведут себя по-разному
Reload перечитывает конфигурацию, но уже открытые долгие соединения не обязаны мгновенно перейти на новую схему. Тестируйте новый поток после reload отдельно и не делайте вывод по старой вкладке браузера. Для диагностики полезны время открытия соединения, время последнего события и причина закрытия на стороне клиента.
Где этот подход ломается
Отключение буферизации не превращает обычный HTTP-обработчик в SSE-сервис. Если backend формирует полный ответ до отправки, Nginx нечего передавать частями. Если приложение не делает flush, данные могут оставаться в буфере языка, фреймворка или веб-сервера.
Есть и инфраструктурные ограничения:
- балансировщик может иметь собственный idle timeout и закрывать тихие соединения;
- CDN может не поддерживать нужную модель потоковой доставки или кэшировать маршрут ошибочно;
- большое число клиентов держит открытые worker-соединения, файловые дескрипторы и память;
- при нескольких экземплярах приложения события должны доставляться через общий брокер или другой механизм координации, иначе клиент может попасть не на тот процесс;
- HTTP/2 меняет транспортную модель и лимиты параллельных потоков, поэтому поведение нужно проверять в реальном браузере, а не только через curl.
Аутентификация, переподключение EventSource и доставка пропущенных событий — отдельные темы. Настройки Nginx сами по себе не обеспечивают восстановление клиентом истории после разрыва.
Когда что выбирать
Для небольшого внутреннего сервиса достаточно отдельного location с отключённой буферизацией, таймаутом по реальному интервалу heartbeat и локально выключенным gzip. Для публичного сервиса сначала посчитайте одновременно открытые соединения, частоту heartbeat и объём событий, затем проверьте лимиты балансировщика и операционной системы.
Не выбирайте таймаут «на глаз». Возьмите максимальный нормальный интервал между сообщениями, добавьте запас и сравните результат со всеми прокси в цепочке. Метрика, которую стоит собирать в проде, — не только число подключений, но и возраст соединений, время с последнего события, доля 499/502/504 и количество переподключений.
Что сделать дальше
Создайте отдельный тестовый маршрут SSE и проверьте его через curl -N -i, затем откройте тот же URL в браузере через EventSource. Зафиксируйте, через сколько секунд приходит первое событие, как долго живёт тихое соединение и что происходит после reload Nginx. После этого нагрузите маршрут количеством подключений, близким к продакшену, и проверьте файловые дескрипторы, память, worker-соединения и таймауты внешнего балансировщика.
1 В этом материале под потоковой передачей понимается выдача HTTP-ответа частями по мере готовности, а не формирование полного тела ответа перед отправкой.
Контент-конвейер: Из одной идеи — текст, изображение, озвучка и ролик. Один заказ вместо четырёх.
Спросить за 60 ₽Источники: Nginx: ngx_http_proxy_module, MDN: Using server-sent events
Частые вопросы
Достаточно ли поставить proxy_buffering off для SSE?
Почему Nginx закрывает SSE через 60 секунд?
Нужно ли отключать gzip для SSE?
Можно ли использовать один location для API и SSE?
Почему curl получает события, а браузер — нет?
Что будет при нескольких экземплярах приложения?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.