Перейти к содержимому

WebSocket vs SSE vs Long Polling: выбираем реалтайм в 2025

Константин Потапов
25 min

Честное сравнение трёх подходов к real-time коммуникации: WebSocket, Server-Sent Events и Long Polling. Когда использовать каждый, подводные камни и production-ready примеры на FastAPI.

WebSocket vs SSE vs Long Polling: выбираем реалтайм в 2025

Когда HTTP перестаёт справляться

Пятница, 18:30. Клиент тестирует новый чат. Обновляет страницу каждые 2 секунды руками. "Почему сообщения приходят с задержкой?" — спрашивает он. Вы смотрите на код:

# ❌ Polling каждые 2 секунды с фронтенда
@app.get("/messages")
async def get_messages(last_id: int = 0):
    return await db.query(
        "SELECT * FROM messages WHERE id > ?", last_id
    )

Проблемы:

  • 30 открытых вкладок → 15 запросов в секунду → сервер умирает
  • Пользователь видит сообщения с задержкой 0-2 секунды (рандом)
  • Если выставить 100ms polling → 600 запросов в минуту от одного пользователя
  • Большая часть запросов возвращает пустой ответ (холостые)

Вы гуглите "real-time python" и находите три решения:

  • WebSocket — "полный дуплекс, самый мощный"
  • Server-Sent Events (SSE) — "простой, работает через HTTP"
  • Long Polling — "fallback для старых браузеров"

Спойлер: Все три живут в production крупных компаний. WebSocket не всегда лучший выбор. SSE недооценён. Long Polling не устарел.

Я провёл последние 2 года, внедряя real-time в production: чаты, дашборды, биржевые котировки, уведомления. Сталкивался с CORS в WebSocket, проблемами SSE за nginx, и внезапным оживлением Long Polling на слабых мобильных сетях.

Сейчас покажу, как выбрать правильный подход для вашей задачи. Без хайпа, только практика.


Участники заезда: кто есть кто

WebSocket: двусторонняя магистраль

Что это: Полноценный TCP-канал поверх HTTP. После handshake работает как постоянное соединение, где клиент и сервер шлют сообщения когда захотят.

Аналогия: Телефонный звонок. Установили связь → оба говорят и слушают одновременно → разговор длится, пока не повесите трубку.

# Клиент
ws = new WebSocket('ws://localhost:8000/ws')
ws.send('Hello')              // Клиент → Сервер
ws.onmessage = (msg) => {...} // Сервер → Клиент
 
# Сервер (FastAPI)
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    await websocket.send_text("Welcome!")  # Сервер → Клиент
    data = await websocket.receive_text()  # Клиент → Сервер

Ключевое отличие: Bidirectional (двусторонний). Сервер может слать данные клиенту без запроса.

Когда блистает:

  • ✅ Чаты (Discord, Telegram Web)
  • ✅ Многопользовательские игры (realtime multiplayer)
  • ✅ Коллаборативные редакторы (Google Docs, Figma)
  • ✅ Биржевые терминалы (обновления котировок каждые 10ms)

Когда бесит:

  • ❌ Load balancer без sticky sessions → соединения рвутся
  • ❌ Прокси/файрволы закрывают idle соединения через 60 секунд
  • ❌ Много открытых вкладок → много соединений → съедает память
  • ❌ CORS на WebSocket настраивается иначе, чем на HTTP

Server-Sent Events (SSE): односторонний поток

Что это: HTTP-соединение, которое сервер держит открытым и шлёт события текстовым потоком. Клиент только слушает.

Аналогия: Радио. Вы включили станцию (открыли соединение) → слушаете эфир (получаете события) → не можете ответить диджею (только сервер → клиент).

# Клиент
const eventSource = new EventSource('/sse')
eventSource.onmessage = (event) => {
  console.log(event.data) // Только получение
}
 
# Сервер (FastAPI)
@app.get("/sse")
async def sse_endpoint(request: Request):
    async def event_stream():
        while True:
            yield f"data: {datetime.now()}\n\n"
            await asyncio.sleep(1)
 
    return StreamingResponse(
        event_stream(),
        media_type="text/event-stream"
    )

Ключевое отличие: Unidirectional (односторонний), только сервер → клиент. Для отправки данных используйте обычный HTTP POST.

Когда блистает:

  • ✅ Уведомления (GitHub notifications, email alerts)
  • ✅ Обновления dashboards (метрики, графики)
  • ✅ Live-логи (tail -f в браузере)
  • ✅ Прогресс долгих операций (загрузка файлов, обработка)

Когда бесит:

  • ❌ Nginx/Apache буферизуют ответ → события не доходят (нужна настройка)
  • ❌ Нет нативной поддержки в fetch API (только EventSource)
  • ❌ Браузер ограничивает 6 SSE на домен (HTTP/1.1)
  • ❌ На мобильных может умереть при переходе в фон

Long Polling: хитрый HTTP

Что это: Клиент делает запрос, сервер держит его открытым до появления данных (или таймаута), отвечает, клиент сразу делает новый запрос.

Аналогия: Очередь в поликлинике. Вы спрашиваете "Мой талон готов?" → если нет, вас просят подождать → как только готов, вам говорят → вы сразу спрашиваете про следующий.

# Клиент
async function poll() {
  const response = await fetch('/poll?last_id=123')
  const data = await response.json()
  processData(data)
  poll() // Сразу следующий запрос
}
 
# Сервер (FastAPI)
@app.get("/poll")
async def long_poll(last_id: int = 0):
    # Ждём до 30 секунд новых данных
    for _ in range(30):
        new_data = await get_new_messages(last_id)
        if new_data:
            return new_data
        await asyncio.sleep(1)
 
    return []  # Timeout, клиент повторит

Ключевое отличие: Обычный HTTP, но с задержкой ответа. Сервер не отвечает мгновенно, а ждёт данные.

Когда блистает:

  • ✅ Когда WebSocket/SSE заблокированы корпоративным прокси
  • ✅ Слабая/нестабильная сеть (2G/3G) → переподключение дешевле
  • ✅ Редкие обновления (раз в минуту) → не нужен постоянный канал
  • ✅ Совместимость со всеми браузерами (даже IE)

Когда бесит:

  • ❌ Высокая latency на установку соединения (SSL handshake каждый раз)
  • ❌ Много параллельных клиентов → много висящих соединений
  • ❌ Сложнее масштабировать (каждый request = worker)

Бой на ринге: сравнение по боли

Раунд 1: Latency (задержка доставки)

Тест: Сервер отправляет событие → замеряем, за сколько оно дошло до клиента.

ТехнологияПервое событиеПоследующиеПочему
WebSocket50ms<1msСоединение уже открыто, данные сразу летят
SSE100ms<1msHTTP overhead, но поток открыт
Long Polling150-300ms50-200msКаждое событие = новый HTTP request (+ SSL)

Вердикт: WebSocket и SSE — мгновенно. Long Polling — ощутимая задержка.

Реальный кейс: Биржевой терминал для криптовалют. WebSocket доставляет обновление котировки BTC за 0.5ms. С Long Polling задержка 100-200ms → арбитражники проиграли бы.


Раунд 2: Трафик (сколько байт улетает)

Тест: 100 событий за минуту, каждое 100 байт полезных данных.

ТехнологияПолезные данныеHTTP headersИтогоOverhead
WebSocket10 KB0.5 KB10.5 KB5%
SSE10 KB1 KB11 KB10%
Long Polling10 KB20-40 KB50 KB200-400% (!!)

Вердикт: WebSocket экономит трафик в 5x. Long Polling — расточитель.

Почему Long Polling так плох:

Каждое событие = полный HTTP request/response цикл:

GET /poll HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0...
Cookie: session=abc123...
Accept: application/json
...ещё 15 строк headers...

HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: ...
Cache-Control: no-cache
...ещё 10 строк headers...

{"message": "hi"}  ← 17 байт данных, 800+ байт headers

С WebSocket после handshake:

\x81\x11{"message": "hi"}  ← 2 байта фрейма + 17 байт данных

Экономия: ~800 байт на каждое событие. При 1M событий в день = 760 MB трафика сэкономлено.


Раунд 3: Нагрузка на сервер (ресурсы)

Тест: 10,000 одновременных клиентов, событие раз в 10 секунд.

ТехнологияОткрытые соединенияCPU (idle)MemoryПочему
WebSocket10,000~5%200 MBДержим соединения, но они спят
SSE10,000~7%250 MBHTTP keep-alive чуть тяжелее
Long Polling10,000~30%400 MBПостоянно обрабатываем новые requests

Вердикт: WebSocket/SSE масштабируются лучше. Long Polling нагружает CPU при reconnect.

Реальный кейс: Уведомления для SaaS (30k пользователей онлайн).

  • С Long Polling: 8 серверов c5.2xlarge ($6k/месяц), CPU usage 60-80%
  • После миграции на SSE: 3 сервера ($2.2k/месяц), CPU usage 20-30%

Экономия: $3.8k/месяц = $45k/год.


Раунд 4: Сложность реализации

Время на MVP (с нуля до работающего кода):

ТехнологияВремяСложность кодаПодводных камней
SSE1 час★☆☆☆☆Прямолинейно, работает из коробки
WebSocket3 часа★★★☆☆Нужен роутинг событий, reconnect, heartbeat
Long Polling2 часа★★☆☆☆Проще WebSocket, но нужна логика timeout

Вердикт: SSE — самый простой старт. WebSocket — требует инфраструктуры.


Грабли production: истории из окопов

WebSocket: "Почему половина пользователей отключаются?"

Кейс: Чат для корпоративного портала. 50% пользователей теряют соединение через 60 секунд.

Причина: Nginx по умолчанию закрывает idle WebSocket через proxy_read_timeout 60s.

Решение 1 — Увеличить таймаут:

location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
 
    # Увеличили до 1 часа
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

Решение 2 — Heartbeat (ping/pong):

# Сервер шлёт ping каждые 30 секунд
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
 
    async def send_ping():
        while True:
            try:
                await websocket.send_text("ping")
                await asyncio.sleep(30)
            except:
                break
 
    asyncio.create_task(send_ping())
 
    # ... обработка сообщений

Клиент:

ws.onmessage = (event) => {
  if (event.data === "ping") {
    ws.send("pong"); // Ответ на heartbeat
    return;
  }
  // обработка реальных сообщений
};

Урок: WebSocket требует активности (heartbeat), иначе прокси решат, что соединение мёртвое.


SSE: "События не доходят до клиента"

Кейс: Dashboard с real-time метриками. События генерируются, но клиент их не видит.

Причина: Nginx буферизует ответ и ждёт накопления данных, прежде чем отдать клиенту.

Решение:

location /sse {
    proxy_pass http://backend;
 
    # Отключаем буферизацию для SSE
    proxy_buffering off;
    proxy_cache off;
 
    # Обязательно для SSE
    proxy_set_header Connection '';
    proxy_http_version 1.1;
    chunked_transfer_encoding off;
 
    # HTTP/1.1 keep-alive
    proxy_read_timeout 86400s;
}

FastAPI (важные headers):

from starlette.responses import StreamingResponse
 
@app.get("/sse")
async def sse(request: Request):
    async def event_generator():
        while True:
            if await request.is_disconnected():
                break
 
            # Формат SSE: "data: <payload>\n\n"
            yield f"data: {json.dumps({'time': time.time()})}\n\n"
            await asyncio.sleep(1)
 
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",  # Для Nginx
        }
    )

Урок: SSE требует правильной настройки прокси и headers.


Long Polling: "Сервер умирает при 1000 пользователей"

Кейс: Уведомления для мобильного приложения. Long Polling с timeout 60s. При 1000 пользователей сервер исчерпывает workers.

Проблема: Gunicorn с 4 workers × 1 thread = 4 одновременных запроса. Остальные 996 ждут в очереди.

Решение 1 — Async workers:

# ❌ Плохо: Sync workers
gunicorn app:app --workers 4 --worker-class sync
 
# ✅ Хорошо: Async workers (gevent или uvicorn)
gunicorn app:app --workers 4 --worker-class uvicorn.workers.UvicornWorker

С async workers один процесс держит тысячи одновременных long-poll соединений.

Решение 2 — Переход на SSE:

Long Polling с частыми событиями (> 1/минуту) → лучше использовать SSE.

Урок: Long Polling требует async runtime или быстро упрётесь в лимит workers.


Production-ready примеры на FastAPI

WebSocket: чат с broadcast

Архитектура:

Клиент 1 → WebSocket → FastAPI → Broadcast → WebSocket → Клиент 2
                                     ↓
                            Клиент 3, 4, ...N

Код (app/chat.py):

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import List
import json
 
app = FastAPI()
 
# Менеджер подключений
class ConnectionManager:
    def __init__(self):
        self.active_connections: List[WebSocket] = []
 
    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)
 
    def disconnect(self, websocket: WebSocket):
        self.active_connections.remove(websocket)
 
    async def broadcast(self, message: str):
        """Отправка всем клиентам"""
        for connection in self.active_connections:
            try:
                await connection.send_text(message)
            except:
                # Если отправка упала — удалить соединение
                self.disconnect(connection)
 
manager = ConnectionManager()
 
@app.websocket("/ws/chat")
async def websocket_chat(websocket: WebSocket):
    await manager.connect(websocket)
 
    try:
        while True:
            # Получаем сообщение от клиента
            data = await websocket.receive_text()
 
            message = json.loads(data)
            username = message.get("username", "Anonymous")
            text = message.get("text", "")
 
            # Broadcast всем
            await manager.broadcast(json.dumps({
                "username": username,
                "text": text,
                "timestamp": time.time()
            }))
 
    except WebSocketDisconnect:
        manager.disconnect(websocket)
        await manager.broadcast(json.dumps({
            "system": f"User left the chat"
        }))

Клиент (JavaScript):

const ws = new WebSocket("ws://localhost:8000/ws/chat");
 
ws.onopen = () => console.log("Connected");
 
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  addMessageToUI(message);
};
 
// Отправка сообщения
function sendMessage(text) {
  ws.send(
    JSON.stringify({
      username: "User123",
      text: text,
    })
  );
}
 
// Reconnect при обрыве
ws.onclose = () => {
  console.log("Disconnected, reconnecting...");
  setTimeout(() => location.reload(), 1000);
};

Подводные камни:

  1. Broadcast всем → O(N) на каждое сообщение. При 10k пользователей это 10k send().
    • Решение: используйте Redis Pub/Sub для координации между серверами.
  2. Нет персистентности. Перезапуск сервера → все отключаются.
    • Решение: храните сообщения в БД, при reconnect загружайте историю.
  3. Один сервер. Горизонтальное масштабирование без координации невозможно.

SSE: Live dashboard с метриками

Use case: Real-time мониторинг метрик (CPU, память, запросы в секунду).

Код (app/dashboard.py):

from fastapi import FastAPI, Request
from sse_starlette.sse import EventSourceResponse
import asyncio
import psutil
import json
 
app = FastAPI()
 
async def generate_metrics():
    """Генератор метрик каждую секунду"""
    while True:
        metrics = {
            "cpu_percent": psutil.cpu_percent(interval=1),
            "memory_percent": psutil.virtual_memory().percent,
            "timestamp": time.time()
        }
 
        # Формат SSE
        yield {
            "event": "metrics",
            "data": json.dumps(metrics)
        }
 
        await asyncio.sleep(1)
 
@app.get("/sse/metrics")
async def sse_metrics(request: Request):
    """SSE endpoint для метрик"""
 
    async def event_stream():
        try:
            async for event in generate_metrics():
                # Проверяем, жив ли клиент
                if await request.is_disconnected():
                    break
 
                # Формат SSE: event: <type>\ndata: <payload>\n\n
                yield f"event: {event['event']}\n"
                yield f"data: {event['data']}\n\n"
 
        except asyncio.CancelledError:
            # Клиент отключился
            pass
 
    return EventSourceResponse(
        event_stream(),
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",
        }
    )

Клиент (JavaScript):

const eventSource = new EventSource("/sse/metrics");
 
eventSource.addEventListener("metrics", (event) => {
  const data = JSON.parse(event.data);
 
  updateChart("cpu", data.cpu_percent);
  updateChart("memory", data.memory_percent);
});
 
eventSource.onerror = () => {
  console.error("SSE connection failed");
  eventSource.close();
 
  // Reconnect через 5 секунд
  setTimeout(() => location.reload(), 5000);
};

Плюсы SSE:

  • ✅ Встроенный reconnect в EventSource
  • ✅ Типизированные события (event: metrics)
  • ✅ Работает через стандартный HTTP (проще CORS)

Минусы SSE:

  • ❌ Только сервер → клиент (для отправки данных нужен POST)
  • ❌ Лимит 6 соединений на домен в HTTP/1.1 (решается HTTP/2)

Решение для отправки данных:

// Получаем метрики через SSE
const eventSource = new EventSource("/sse/metrics");
 
// Отправляем команды через fetch
async function restartService() {
  await fetch("/api/restart", { method: "POST" });
}

Long Polling: уведомления с редкими обновлениями

Use case: Уведомления приходят редко (раз в 5-10 минут), держать постоянное соединение избыточно.

Код (app/notifications.py):

from fastapi import FastAPI
import asyncio
 
app = FastAPI()
 
# In-memory очередь уведомлений (в проде — Redis/DB)
notifications_queue = []
 
@app.get("/poll/notifications")
async def poll_notifications(last_id: int = 0):
    """
    Long polling: ждём до 30 секунд новых уведомлений
    """
    timeout_seconds = 30
 
    for _ in range(timeout_seconds):
        # Проверяем новые уведомления
        new_notifications = [
            n for n in notifications_queue
            if n["id"] > last_id
        ]
 
        if new_notifications:
            return {"notifications": new_notifications}
 
        # Ждём 1 секунду перед следующей проверкой
        await asyncio.sleep(1)
 
    # Timeout — возвращаем пустой ответ
    return {"notifications": []}
 
@app.post("/notifications")
async def create_notification(text: str):
    """Создать уведомление (для теста)"""
    notification = {
        "id": len(notifications_queue) + 1,
        "text": text,
        "timestamp": time.time()
    }
    notifications_queue.append(notification)
    return notification

Клиент (JavaScript):

let lastNotificationId = 0;
 
async function pollNotifications() {
  try {
    const response = await fetch(
      `/poll/notifications?last_id=${lastNotificationId}`
    );
    const data = await response.json();
 
    if (data.notifications.length > 0) {
      data.notifications.forEach(showNotification);
      lastNotificationId = Math.max(...data.notifications.map((n) => n.id));
    }
  } catch (error) {
    console.error("Polling failed:", error);
    // Backoff при ошибке
    await new Promise((resolve) => setTimeout(resolve, 5000));
  }
 
  // Сразу следующий запрос
  pollNotifications();
}
 
// Старт
pollNotifications();

Оптимизация — Redis Pub/Sub для масштабирования:

Проблема: при 1000 одновременных long-poll запросов сервер выполняет 1000 × 30 = 30,000 проверок БД в секунду.

Решение:

import aioredis
 
redis = aioredis.from_url("redis://localhost")
 
@app.get("/poll/notifications")
async def poll_notifications(user_id: int, last_id: int = 0):
    """Long polling через Redis Pub/Sub"""
 
    # Подписываемся на канал пользователя
    pubsub = redis.pubsub()
    await pubsub.subscribe(f"user:{user_id}:notifications")
 
    # Ждём событие или timeout
    try:
        async with asyncio.timeout(30):
            async for message in pubsub.listen():
                if message["type"] == "message":
                    notification = json.loads(message["data"])
                    return {"notifications": [notification]}
 
    except asyncio.TimeoutError:
        return {"notifications": []}
 
    finally:
        await pubsub.unsubscribe()
 
# При создании уведомления — publish в Redis
@app.post("/notifications")
async def create_notification(user_id: int, text: str):
    notification = {"id": generate_id(), "text": text}
 
    # Публикуем в канал пользователя
    await redis.publish(
        f"user:{user_id}:notifications",
        json.dumps(notification)
    )
 
    return notification

Теперь сервер не опрашивает БД в цикле, а просто слушает Redis. Масштабируется на миллионы пользователей.


Чек-лист: что выбрать за 60 секунд

Выбирайте WebSocket, если ответили "ДА" на 3+ вопроса:

  • Нужна двусторонняя связь (клиент ↔ сервер)?
  • События приходят часто (> 10/минуту)?
  • Критична минимальная latency (< 10ms)?
  • Готовы настраивать инфраструктуру (nginx, reconnect, heartbeat)?
  • Пользователи долго на странице (> 5 минут)?

Примеры: Чаты, multiplayer игры, коллаборативные редакторы, биржевые терминалы.

Архитектура:

Клиент (JS) ←→ WebSocket ←→ FastAPI ←→ Redis Pub/Sub ←→ Другие серверы

Выбирайте SSE, если ответили "ДА" на 3+ вопроса:

  • Нужна только отправка с сервера (сервер → клиент)?
  • События приходят регулярно (несколько раз в минуту)?
  • Хотите простоту (меньше кода, чем WebSocket)?
  • Нужна совместимость с HTTP (CORS, прокси)?
  • Не критична двусторонняя связь (для отправки — обычный POST)?

Примеры: Dashboards, уведомления, live-логи, прогресс задач, новостные ленты.

Архитектура:

Клиент (JS EventSource) ← SSE ← FastAPI ← Database/Redis
                            ↑
                     POST для команд

Выбирайте Long Polling, если ответили "ДА" на 3+ вопроса:

  • События приходят редко (< 1/минуту)?
  • Работаете через корпоративные прокси (WebSocket/SSE блокируются)?
  • Нужна совместимость со старыми браузерами?
  • Слабая сеть (2G/3G) → частые обрывы соединения?
  • Простая логика — "есть данные → отдать, нет → подождать"?

Примеры: Редкие уведомления, мобильные приложения на слабой сети, legacy-системы.

Архитектура:

Клиент (fetch) → Long Poll (30s timeout) → FastAPI → Redis Pub/Sub
                       ↓
                 Ответ или timeout
                       ↓
                 Новый запрос

Таблица принятия решения

КритерийWebSocketSSELong Polling
Latency< 1ms< 1ms50-200ms
ТрафикМинимумНизкийВысокий
Двусторонняя связь
Простота реализации★★★☆☆★☆☆☆☆★★☆☆☆
Нагрузка на серверНизкаяНизкаяСредняя
Совместимость95%98%100%
Работа через прокси⚠️
Reconnect из коробки
МасштабированиеСложноеСреднееПростое
Use caseЧатыDashboardsNotifications

Гибридный подход: лучшее из всех миров

Реальная практика: Используйте несколько технологий в одном приложении.

Пример: SaaS dashboard

from fastapi import FastAPI
 
app = FastAPI()
 
# WebSocket для чата support
@app.websocket("/ws/support")
async def support_chat(websocket: WebSocket):
    # Двусторонний чат с операторами
    ...
 
# SSE для real-time метрик
@app.get("/sse/metrics")
async def metrics_stream(request: Request):
    # Обновление графиков каждые 5 секунд
    ...
 
# Long Polling для редких уведомлений
@app.get("/poll/notifications")
async def poll_notifications(last_id: int):
    # Уведомления о завершении задач
    ...
 
# Обычный REST для всего остального
@app.get("/api/users")
async def get_users():
    ...

Зачем так сложно?

  • Чат требует instant latency → WebSocket
  • Метрики обновляются регулярно → SSE проще
  • Уведомления редкие → Long Polling экономит ресурсы
  • CRUD операции → обычный REST

Результат: Каждая технология там, где она сильнее всего.


Мониторинг и отладка

WebSocket метрики

from prometheus_client import Counter, Histogram
 
ws_connections = Counter('ws_connections_total', 'WebSocket connections')
ws_messages = Counter('ws_messages_total', 'WebSocket messages sent')
ws_latency = Histogram('ws_message_latency_seconds', 'Message latency')
 
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    ws_connections.inc()
 
    start = time.time()
    await websocket.send_text("hello")
    ws_latency.observe(time.time() - start)
 
    ws_messages.inc()

Ключевые метрики:

  • active_connections — текущие подключения
  • messages_per_second — throughput
  • reconnect_rate — как часто клиенты переподключаются
  • message_latency — задержка доставки

SSE debugging

import logging
 
logger = logging.getLogger(__name__)
 
@app.get("/sse")
async def sse(request: Request):
    client_id = request.client.host
    logger.info(f"SSE connected: {client_id}")
 
    async def event_stream():
        try:
            while True:
                yield f"data: {time.time()}\n\n"
                await asyncio.sleep(1)
        except Exception as e:
            logger.error(f"SSE error for {client_id}: {e}")
        finally:
            logger.info(f"SSE disconnected: {client_id}")
 
    return StreamingResponse(event_stream(), media_type="text/event-stream")

Частые проблемы:

  • Клиент не видит события → проверьте nginx buffering
  • Reconnect каждые 30 секунд → увеличьте proxy_read_timeout
  • События дублируются → проверьте логику reconnect на клиенте

Итоги: что я вынес из production

После 2 лет работы с real-time в production вот что я знаю точно:

  1. SSE недооценён. В 80% кейсов "нужен WebSocket" на самом деле хватит SSE. Проще, надёжнее, меньше граблей.

  2. WebSocket — это инфраструктура. Если решились, будьте готовы настраивать nginx, писать reconnect логику, мониторить heartbeat, координировать через Redis. Но когда нужна минимальная latency — альтернативы нет.

  3. Long Polling жив. На мобильных в слабой сети (2G/3G) он работает стабильнее WebSocket. А для редких событий (раз в минуту) — самый экономный по ресурсам.

  4. Начинайте с простого. Не делайте WebSocket "потому что модно". Сделайте обычный HTTP polling → если станет узким горлом → переходите на SSE → если и его не хватит → тогда WebSocket.

  5. Тестируйте на мобильных. Desktop-браузеры держат WebSocket/SSE часами. Мобильный Safari закроет соединение через 30 секунд фона. Учитывайте это.


Следующий шаг: Выберите один подход, внедрите за выходные, измерьте метрики. Не пытайтесь сразу сделать "правильно" — сделайте работающее, потом оптимизируйте.

P.S. Если после статьи всё равно не знаете, что выбрать — начните с SSE. Серьёзно. Это как git commit в мире real-time: работает в 80% случаев, остальные 20% — исключения.


Полезные ссылки:


Делитесь опытом

Я рассказал свой опыт с real-time в production. Теперь ваша очередь:

  • Какую технологию используете вы?
  • С какими граблями столкнулись?
  • Что бы сделали иначе, зная то, что знаете сейчас?

Пишите в комментариях или в Telegram. Обсудим, сравним, посмеёмся над ошибками.

Нужна консультация по выбору real-time решения? Пишите на почту — разберу ваш кейс и дам честную рекомендацию. Без продаж, без воды, только практика.


Подписывайтесь на обновления в Telegram — пишу про Python, архитектуру и боль разработки. Без воды, только практика.