Если SSE-события приходят не по одному, а пачками после заметной паузы, соединение обычно не оборвано: один из слоёв копит небольшие фрагменты ответа и отдаёт их вместе. Проверяйте цепочку по порядку: приложение → nginx → внешний прокси или CDN → браузер.
Начните с прямого запроса к приложению. Если поток задерживается уже там, исправляйте формирование ответа и сброс буферов; если прямой поток ровный, сравнивайте его с ответом через каждый следующий прокси.
Сначала исключите ошибку формата SSE
Для SSE нужен ответ с типом text/event-stream. Каждое событие состоит из строк полей и заканчивается пустой строкой. Пока разделитель не получен, EventSource не обязан передавать сообщение обработчику, поэтому пропущенная пустая строка выглядит точно как буферизация.
Добавьте в тестовые события последовательный номер и серверное время. В браузере записывайте номер и время получения. Так станет видно, где возникла задержка, а не только момент, когда интерфейс обновился.
Пример минимального события:
id: 17
event: status
data: {"state":"ready","sentAt":"SERVER_TIMESTAMP"}
После строки data здесь есть пустая строка. Не помещайте в тестовый поток токены, персональные данные и внутренние адреса.
Шаг 1. Проверьте приложение без публичного прокси
Запустите запрос с того же сервера или из разрешённой внутренней сети прямо к upstream-приложению:
curl -N -i http://127.0.0.1:PORT/events
Ключ -N отключает буферизацию вывода у самого curl. Смотрите не только на заголовки: несколько тестовых событий должны появляться с ожидаемым интервалом, пока соединение остаётся открытым.
Если события уже на этом шаге выходят пачками, проверьте:
- заканчивается ли каждое событие пустой строкой;
- действительно ли фреймворк передаёт очередной фрагмент в веб-сервер, а не держит всё тело ответа;
- нет ли буфера вывода в PHP, приложении или middleware;
- не собирает ли компрессор маленькие фрагменты перед отправкой;
- не удерживает ли долгий запрос сессию и связанные запросы пользователя.
Одного вызова функции flush может быть недостаточно: данные способны остаться в буфере более высокого уровня. Повторяйте тест после каждого изменения и отмечайте первый слой, на котором интервалы стали ровными.
Шаг 2. Сравните ответ через nginx
Когда прямой upstream работает правильно, повторите тот же тест через локальный адрес nginx. Для SSE-маршрута конфигурация обычно должна отключать буферизацию ответа и кэширование именно в этом location:
location /events {
proxy_pass http://app;
proxy_buffering off;
proxy_cache off;
}
Не отключайте буферизацию для всего сайта: обычным страницам она может быть полезна. И не пытайтесь лечить поток только уменьшением proxy_buffers — это меняет размер накопления, но не устраняет сам режим.
Приложение также может вернуть X-Accel-Buffering: no. nginx использует этот заголовок для управления буферизацией, но обычно не передаёт служебные X-Accel-* клиенту. Поэтому отсутствие заголовка в DevTools ещё не доказывает, что приложение его не отправило: проверяйте ответ upstream и эффективную конфигурацию nginx отдельно.
Если подозрение падает на сжатие, временно отключите его только для тестового SSE-маршрута и снова сравните интервалы. Перед применением конфигурации выполните nginx -t, затем используйте штатную перезагрузку и держите готовым откат.
Шаг 3. Найдите внешний слой, который собирает чанки
Если через локальный nginx события идут ровно, а через публичный домен — пачками, задержка появилась дальше: в балансировщике, WAF, ingress-контроллере, CDN или ещё одном reverse proxy.
Сравнивайте один и тот же тестовый поток с одинаковыми номерами событий. Для внешнего слоя проверьте поддержку потоковых ответов, кэширование, преобразование и сжатие тела, а также ограничения долгих соединений. Контрольный обход CDN выполняйте только из доверенной среды и не публикуйте origin-адрес.
Результат удобно фиксировать так:
| Где наблюдается пачка | Вероятный участок | | --- | --- | | Уже на прямом upstream | приложение или его runtime | | Upstream ровный, локальный nginx задерживает | конфигурация nginx | | nginx ровный, публичный домен задерживает | внешний прокси, WAF или CDN | | Публичный curl -N ровный, интерфейс обновляется пачкой | обработчик или отрисовка в браузере |
Не перепутайте буферизацию с таймаутом
Буферизация задерживает данные, а таймаут разрывает молчащее соединение. У nginx proxy_read_timeout относится к паузе между последовательными чтениями от upstream, а не к общей длительности SSE-запроса.
Если события могут долго не появляться, сервер может периодически отправлять комментарий SSE вида : keepalive с пустой строкой после него. Браузер не создаст пользовательское событие из такого комментария, но по цепочке пройдут байты. Интервал выбирайте по самому строгому известному idle timeout; heartbeat не заменяет отключение буферизации.
Проверьте исправление до полного включения
Проведите тест на ограниченной группе запросов и подтвердите четыре вещи:
- события приходят по одному и в правильном порядке;
- соединение переживает обычную паузу между событиями;
- после краткого обрыва клиент корректно переподключается;
- изменение не создало лишнюю нагрузку по памяти и числу открытых соединений.
Web-Puls помогает регулярно проверять доступность публичного сайта, но причину задержки внутри SSE-цепочки нужно искать послойным сравнением. Если у вас несколько прокси и безопасно проверить цепочку самостоятельно не получается, отправьте заявку на профессиональную диагностику: укажите публичный URL, время и часовой пояс проверки, ожидаемый интервал событий, схему прокси и обезличенные результаты тестов.