Вы настроили Server-Sent Events (SSE), сервер отправляет сообщения каждые 250 мс, но клиент видит их только через 750 мс. Байты приходят вовремя, а события — с опозданием. Эта статья о том, как найти причину: незавершённый кадр SSE или gzip-сжатие без промежуточного сброса. Оба дают одинаковый симптом — задержку, но проявляются в разных точках конвейера. Разберём, как собрать диагностический стенд, какие метки времени ставить и как интерпретировать результаты.

Почему байты есть, а события нет

SSE — это текстовый протокол поверх HTTP. Сервер отправляет события в формате data: ...\n\n. Клиент читает поток и парсит кадры, разделённые двумя переводами строки. Если кадр не завершён (нет пустой строки), парсер не выдаёт событие, хотя байты уже получены. Другая причина — сжатие gzip: сервер может буферизовать данные до заполнения блока или до явного сброса, и клиент получает сжатый поток с задержкой. В обоих случаях вы видите задержку между приходом байтов и появлением события, но источник разный.

Собираем стенд для диагностики

Для воспроизведения используйте стандартную библиотеку Python. Сервер на http.server или Flask отправляет три сообщения с паузой 250 мс. Важно: отключите кэширование и явно управляйте сжатием. Клиент — скрипт на Python с requests или http.client, который читает поток по частям. Установите три метки времени:

  • t1 — момент получения порции байтов из сокета (сырые данные).
  • t2 — момент после декодера gzip (если сжатие включено).
  • t3 — момент, когда парсер SSE выдал готовое событие.

Разница между t1 и t2 покажет задержку на уровне сжатия, между t2 и t3 — на уровне парсинга кадров. Если t1 и t2 близки, а t3 запаздывает — проблема в кадрах. Если t2 отстаёт от t1 — виноват gzip.

Типичные ошибки и как их исправить

Ошибка 1: сервер не отправляет пустую строку после data. Клиент ждёт завершения кадра. Решение: убедитесь, что каждое событие заканчивается двумя переводами строки (\n\n). Ошибка 2: gzip включён, но нет сброса буфера. Сервер копит данные до заполнения блока. Решение: используйте flush() после каждого события или отключите сжатие для SSE. Ошибка 3: клиент читает поток целиком, а не построчно. Решение: читайте по мере поступления, например, через iter_lines() в requests или readline() в http.client. Ошибка 4: прокси или CDN буферизуют ответ. Решение: проверьте заголовки X-Accel-Buffering: no и Cache-Control: no-cache.

Пример диагностики: что покажут метки

Допустим, сервер отправляет три события с паузой 250 мс. Без сжатия и с корректными кадрами клиент должен получить события примерно через 0, 250 и 500 мс. Если вы видите все три события через 750 мс, значит, они накопились и пришли одним пакетом. Проверьте t1: если байты пришли раньше, а события позже — проблема в парсере (незавершённые кадры). Если байты тоже пришли позже — смотрите на gzip и буферизацию на сервере. Включите логирование на каждом этапе: сырые байты, распакованный текст, готовые события. Сравните временные метки.

Для чистоты эксперимента отключите gzip и повторите. Если задержка исчезла — виновато сжатие. Если осталась — ищите ошибку в формировании кадров. Также проверьте, не буферизует ли вывод сам сервер (например, в Python print без flush).

Что делать, чтобы не повторять

  • Всегда завершайте кадр SSE пустой строкой.
  • Для SSE отключайте gzip или настройте сброс буфера после каждого события.
  • Используйте потоковое чтение на клиенте, не ждите закрытия соединения.
  • Добавьте заголовки, отключающие буферизацию на прокси.
  • Пишите логи с метками времени на каждом этапе конвейера.

Следуя этим шагам, вы быстро найдёте причину задержки и сделаете SSE предсказуемым.