Когда HTTP уже не хватает

Стандартный HTTP работает по схеме «запрос → ответ». Клиент спросил — сервер ответил. Никаких инициатив со стороны сервера.

Но когда нужно, чтобы сервер сам отправлял данные — чат, уведомления, курсы валют, статус заказа — эта модель ломается. Нужно что-то другое.

Вот три решения, которые используют почти все: long polling, SSE (Server-Sent Events) и WebSocket. Каждое решает задачу по-своему, и у каждого своё место.

Long Polling — имитация реального времени

Long polling — это хак поверх обычного HTTP. Клиент отправляет запрос, сервер его не закрывает, пока не появятся новые данные. Как только данные есть — отвечает, клиент сразу делает новый запрос.

Схема простая:

  1. Клиент: GET /updates
  2. Сервер держит соединение открытым (секунды, минуты)
  3. Данные появились → сервер отвечает
  4. Клиент немедленно делает новый GET /updates
  5. Повторяем

Плюсы:

  • Работает везде — любой браузер, любой прокси, никаких специальных настроек.
  • Простая реализация на стороне клиента: обычный 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. Обновления частые (чаще раза в секунду) или критична минимальная задержка?

  • Нет → SSE
  • Да → WebSocket

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 только потому что это звучит солиднее. Выбирайте технологию под задачу, а не под резюме.