WebSockets, SSE или polling: что выбрать для real-time
Продвинутое4 минРедакция Innovialabs
Коротко
Для двустороннего обмена (чаты, совместное редактирование, игры) берите WebSockets; для потока событий только от сервера (уведомления, дашборды, стриминг ответа AI) — SSE; polling оставьте как простой запасной вариант.
Короткий ответ
| Задача | Лучший выбор |
|---|---|
| Чат, совместное редактирование, мультиплеер | WebSockets |
| Уведомления, лента событий, live-дашборд | SSE |
| Стриминг ответа LLM по токенам | SSE (или стриминг HTTP-ответа) |
| Редкие обновления, статус фоновой задачи | Polling |
| Окружение, где ничего кроме обычного HTTP не проходит | Long polling |
Главный вопрос — кто кому пишет. Если данные идут только от сервера к клиенту, WebSockets обычно избыточны.
Как работает каждый механизм
Short polling. Клиент раз в N секунд спрашивает: «есть новое?». Просто и надёжно, но задержка равна интервалу, а большинство запросов возвращаются пустыми.
Long polling. Клиент отправляет запрос, сервер держит его открытым, пока не появятся данные или не истечёт таймаут. После ответа клиент сразу отправляет новый запрос. Задержка ниже, но каждое сообщение — это новый HTTP-цикл с заголовками.
Server-Sent Events (SSE). Клиент открывает обычный HTTP-запрос, сервер отвечает с Content-Type: text/event-stream и не закрывает соединение, отправляя события текстом. Направление — только сервер → клиент. В браузере есть встроенный EventSource с автоматическим переподключением.
const es = new EventSource("/api/events");
es.onmessage = (e) => console.log(JSON.parse(e.data));
WebSockets. Соединение начинается как HTTP и переключается (Upgrade) на отдельный протокол. Дальше обе стороны отправляют сообщения в любой момент, текстовые или бинарные, с минимальными накладными расходами.
const ws = new WebSocket("wss://example.com/chat");
ws.onmessage = (e) => render(JSON.parse(e.data));
ws.send(JSON.stringify({ text: "Привет" }));
Сравнение
| Polling | Long polling | SSE | WebSockets | |
|---|---|---|---|---|
| Направление | Клиент → сервер | Клиент → сервер | Сервер → клиент | Оба |
| Задержка | Интервал опроса | Низкая | Низкая | Низкая |
| Протокол | HTTP | HTTP | HTTP | Отдельный, после Upgrade |
| Переподключение | Не нужно | Вручную | Встроено в EventSource | Вручную |
| Бинарные данные | Да | Да | Нет, только текст | Да |
| Совместимость с прокси | Отличная | Хорошая | Хорошая, если отключена буферизация | Нужна настройка |
Прокси, балансировщики и инфраструктура
Здесь чаще всего и ломается real-time.
- Буферизация. nginx и другие прокси могут буферизовать ответ, и события SSE придут пачкой. Для SSE отключайте буферизацию (в nginx —
proxy_buffering offили заголовокX-Accel-Buffering: no). - Таймауты простоя. Прокси и балансировщики закрывают «тихие» соединения. Отправляйте heartbeat: комментарий
:в SSE или ping/pong в WebSockets, и увеличивайтеproxy_read_timeout. - Upgrade для WebSockets. Прокси должен пробрасывать заголовки
UpgradeиConnection, иначе рукопожатие не пройдёт. - HTTP/1.1 и лимит соединений. Браузер ограничивает число одновременных соединений к одному домену по HTTP/1.1, и несколько вкладок с SSE могут его исчерпать. По HTTP/2 потоки мультиплексируются, и проблема уходит.
- Serverless. Многие serverless-платформы ограничивают длительность запроса и плохо подходят для долгоживущих WebSocket-соединений. SSE с ограниченной длительностью работает чаще.
Масштабирование
Долгоживущее соединение привязано к конкретному серверу. Когда серверов несколько:
- Общая шина сообщений. Событие, возникшее на сервере A, должно дойти до клиента, подключённого к серверу B. Используют Redis Pub/Sub, NATS, Kafka или управляемые сервисы.
- Sticky sessions. Нужны для long polling и некоторых библиотек с fallback-механизмом, чтобы запросы клиента попадали на тот же сервер.
- Лимиты ресурсов. Каждое соединение занимает память и файловый дескриптор. Проверьте лимиты ОС и выбирайте асинхронный сервер.
- Плавный деплой. При перезапуске все клиенты переподключатся одновременно. Добавьте случайную задержку (jitter) в переподключение.
Переподключение и потеря сообщений
Соединения рвутся: смена сети, сон ноутбука, деплой. Планируйте это заранее.
- SSE: отправляйте поле
id:у событий. При переподключении браузер пришлёт заголовокLast-Event-ID, и сервер сможет дослать пропущенное. - WebSockets: переподключение пишется вручную — с экспоненциальной задержкой и jitter. После переподключения клиент запрашивает изменения с последнего известного момента.
- Источник правды — сервер. Real-time-канал лучше считать способом доставки уведомлений, а актуальное состояние получать отдельным запросом.
Частые ошибки
- WebSockets там, где хватило бы SSE, а с ними — лишняя инфраструктура.
- Нет heartbeat, и соединения тихо умирают через минуту на прокси.
- Нет авторизации при установке соединения или проверка токена только один раз на часы работы.
- Масштабирование на несколько серверов без общей шины: часть клиентов не получает события.