Когда HTTP уже не хватает
Стандартный HTTP работает по схеме «запрос → ответ». Клиент спросил — сервер ответил. Никаких инициатив со стороны сервера.
Но когда нужно, чтобы сервер сам отправлял данные — чат, уведомления, курсы валют, статус заказа — эта модель ломается. Нужно что-то другое.
Вот три решения, которые используют почти все: long polling, SSE (Server-Sent Events) и WebSocket. Каждое решает задачу по-своему, и у каждого своё место.
Long Polling — имитация реального времени
Long polling — это хак поверх обычного HTTP. Клиент отправляет запрос, сервер его не закрывает, пока не появятся новые данные. Как только данные есть — отвечает, клиент сразу делает новый запрос.
Схема простая:
- Клиент:
GET /updates
- Сервер держит соединение открытым (секунды, минуты)
- Данные появились → сервер отвечает
- Клиент немедленно делает новый
GET /updates
- Повторяем
Плюсы:
- Работает везде — любой браузер, любой прокси, никаких специальных настроек.
- Простая реализация на стороне клиента: обычный
fetch или XMLHttpRequest.
- Не нужно менять инфраструктуру: балансировщики, файрволы — всё понимает обычный HTTP.
Минусы:
- Каждое «обновление» — это новый HTTP-запрос со всеми заголовками.
- Задержка: минимум один round-trip на каждом цикле.
- При большом количестве клиентов нагрузка на сервер растёт быстро.
- Если у сервера нет данных 30 секунд — соединение всё равно нужно закрыть по таймауту, клиент делает новый запрос. Трафик есть, пользы нет.
Когда использовать:
Long polling хорош там, где нет другого выхода: старые корпоративные сети с агрессивными прокси, legacy-системы, окружения где WebSocket заблокирован. Ещё одна ниша — задачи с редкими обновлениями. Проверка статуса фоновой задачи раз в несколько секунд — вполне разумно.
Реальный пример: система уведомлений в старом интранете компании, где IT-отдел не разрешает ничего кроме 80 и 443 портов через корпоративный прокси. Long polling работает без вопросов.
async function poll() {
try {
const res = await fetch('/api/updates?lastId=' + lastId);
const data = await res.json();
processUpdates(data);
lastId = data.lastId;
} finally {
setTimeout(poll, 100);
}
}
SSE — однонаправленный поток от сервера
SSE (Server-Sent Events) — стандарт HTML5, который многие недооценивают. Клиент открывает одно HTTP-соединение, и сервер льёт в него данные когда хочет. Соединение не закрывается.
Формат простой. Сервер отправляет текст вида:
data: {"price": 95.32}\n\n
data: {"price": 95.41}\n\n
Двойной перенос строки — разделитель событий. Браузер читает это через EventSource API:
const es = new EventSource('/api/stream');
es.onmessage = (e) => {
const data = JSON.parse(e.data);
updateUI(data);
};
Три строки кода на клиенте.
Плюсы:
- Нативная поддержка в браузере — не нужна никакая библиотека.
- Автоматическое переподключение при обрыве.
- Встроенная нумерация событий через
id: — клиент может восстановиться с места разрыва.
- Работает поверх обычного HTTP/1.1 и HTTP/2.
- Меньше накладных расходов, чем у long polling.
- Корректно обрабатывается прокси и балансировщиками — это обычный HTTP GET.
Минусы:
- Только сервер → клиент. Клиент не может отправлять данные через это же соединение.
- Нет бинарных данных — только текст (base64 работает, но это костыль).
- В браузере лимит: одновременно не более 6 SSE-соединений к одному домену при HTTP/1.1. С HTTP/2 этот лимит исчезает.
Когда использовать:
SSE — идеальный выбор для однонаправленных потоков данных. Курсы валют, ленты новостей, прогресс загрузки файла, live-логи, стриминг ответов от LLM (именно так работает ChatGPT — вы видите, как текст печатается). Уведомления в личном кабинете — тоже сюда.
Главный вопрос: «нужно ли клиенту что-то отправлять через это соединение?» Если нет — берите SSE.
WebSocket — полноценный двусторонний канал
WebSocket — это уже другой протокол. Не HTTP, хотя рукопожатие начинается с него через заголовок Upgrade: websocket. После установки соединения — постоянный двусторонний канал с минимальными накладными расходами.
Разница в заголовках огромная. HTTP-запрос — это 300–900 байт заголовков на каждое сообщение. WebSocket-фрейм добавляет всего 2–14 байт служебных данных. При частой отправке мелких сообщений это принципиально.
const ws = new WebSocket('wss://api.example.com/ws');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
handleMessage(msg);
};
Плюсы:
- Полный дуплекс: и сервер, и клиент могут отправлять данные в любой момент.
- Минимальные накладные расходы на фрейм.
- Поддержка бинарных данных.
- Низкая задержка — нет нужды в новых HTTP-рукопожатиях.
- Хорошо подходит для высокочастотных обновлений — тысячи сообщений в секунду.
Минусы:
- Сложнее инфраструктура: балансировщики должны поддерживать sticky sessions или pub/sub для горизонтального масштабирования.
- Некоторые прокси обрывают долгоживущие соединения — корпоративные сети, частая проблема.
- Нет встроенного переподключения: нужно реализовывать самому или брать библиотеку вроде socket.io.
- Stateful: каждое соединение — это состояние на сервере. При падении сервера все клиенты должны переподключиться.
- Масштабирование сложнее: если несколько серверов, нужен Redis pub/sub или аналог, чтобы сообщение от одного клиента дошло до другого на другом сервере.
Когда использовать:
Чаты, многопользовательские игры, коллаборативное редактирование (как в Figma или Google Docs), торговые терминалы, живые аукционы — всё, где нужен высокочастотный двусторонний обмен. Если задержка критична и клиент активно отправляет данные — WebSocket.
Сравнение в цифрах
Грубые ориентиры для 1000 одновременных клиентов, получающих обновления раз в секунду:
Long polling:
- 1000 HTTP-запросов в секунду
- ~300–600 байт заголовков на запрос
- ~300–600 КБ трафика только на заголовки каждую секунду
- Нагрузка на балансировщик: высокая
SSE:
- 1000 постоянных соединений
- ~5–50 байт на сообщение (только данные + разделитель)
- Заголовки — разовые при подключении
- Нагрузка на балансировщик: умеренная
WebSocket:
- 1000 постоянных соединений
- 2–14 байт фрейм + данные
- Трафик: минимальный из трёх
- Нагрузка на балансировщик: умеренная, но нужна поддержка WS
Как выбрать: дерево решений
1. Нужно ли клиенту отправлять данные через это же соединение в реальном времени?
- Нет → переходи к пункту 2
- Да → WebSocket
2. Обновления частые (чаще раза в секунду) или критична минимальная задержка?
3. Есть ограничения инфраструктуры (старые прокси, корпоративные сети)?
- Да → Long polling
- Нет → SSE
Если коротко: SSE закрывает 70% задач. Уведомления, стриминг, ленты — берите SSE, не переусложняйте. WebSocket нужен для чатов и коллаборации. Long polling — когда всё остальное заблокировано.
Комбинированный подход
В реальных проектах часто используют несколько технологий. Типичная схема: чат на WebSocket для обмена сообщениями, SSE для уведомлений (кто-то вошёл в систему, новый документ), HTTP REST для загрузки истории.
Ещё популярная схема — фолбэк. Подключаемся через WebSocket, если не получилось — переходим на SSE, если и это не работает — long polling. Именно так устроен socket.io. Но если вы не поддерживаете экзотические окружения, этот оверхед часто не нужен: просто выбирайте что-то одно.
Реализация на сервере
Для Node.js:
- SSE — встроен в любой HTTP-сервер, никаких библиотек
- WebSocket — библиотека
ws (минималистичная) или socket.io (с фолбэком и комнатами)
Для Python:
- SSE —
sse-starlette для FastAPI
- WebSocket — встроен в FastAPI/Starlette
Для Go:
- WebSocket —
gorilla/websocket или встроенный пакет начиная с Go 1.21
Отдельная тема — масштабирование WebSocket. Если у вас несколько инстансов приложения, нужен брокер сообщений. Redis pub/sub — самое распространённое решение: каждый сервер подписывается на нужные каналы, и когда клиент шлёт сообщение на сервер A, оно через Redis попадает на сервер B, где сидит получатель.
Безопасность
Для WebSocket и SSE используйте wss:// и https:// соответственно — это TLS, без него данные в открытом виде.
Для WebSocket дополнительно: при рукопожатии браузер отправляет заголовок Origin. Проверяйте его на сервере, чтобы не допустить cross-site WebSocket hijacking.
SSE в этом плане проще — работает через обычный HTTP, все механизмы безопасности браузера (CORS, cookies) применяются стандартно.
Long polling — обычный HTTP, никаких дополнительных нюансов.
Пример из практики
При разработке корпоративных порталов в REEXY (r3xy.ru) чаще всего ставим SSE для уведомлений — просто, надёжно, не требует специальной настройки Nginx. WebSocket берётся только когда клиент явно просит чат или живое совместное редактирование.
Один из недавних кейсов: дашборд для склада, где менеджер видит статус заказов в реальном времени. Обновления только от сервера к клиенту, частота — раз в несколько секунд. Выбрали SSE, написали серверную часть на 40 строк кода. Работает без нареканий уже полгода.
Правило простое: не берите WebSocket только потому что это звучит солиднее. Выбирайте технологию под задачу, а не под резюме.