Когда 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 (задержка доставки)
Тест: Сервер отправляет событие → замеряем, за сколько оно дошло до клиента.
| Технология | Первое событие | Последующие | Почему |
|---|---|---|---|
| WebSocket | 50ms | <1ms | Соединение уже открыто, данные сразу летят |
| SSE | 100ms | <1ms | HTTP overhead, но поток открыт |
| Long Polling | 150-300ms | 50-200ms | Каждое событие = новый HTTP request (+ SSL) |
Вердикт: WebSocket и SSE — мгновенно. Long Polling — ощутимая задержка.
Реальный кейс: Биржевой терминал для криптовалют. WebSocket доставляет обновление котировки BTC за 0.5ms. С Long Polling задержка 100-200ms → арбитражники проиграли бы.
Раунд 2: Трафик (сколько байт улетает)
Тест: 100 событий за минуту, каждое 100 байт полезных данных.
| Технология | Полезные данные | HTTP headers | Итого | Overhead |
|---|---|---|---|---|
| WebSocket | 10 KB | 0.5 KB | 10.5 KB | 5% |
| SSE | 10 KB | 1 KB | 11 KB | 10% |
| Long Polling | 10 KB | 20-40 KB | 50 KB | 200-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 | Почему |
|---|---|---|---|---|
| WebSocket | 10,000 | ~5% | 200 MB | Держим соединения, но они спят |
| SSE | 10,000 | ~7% | 250 MB | HTTP keep-alive чуть тяжелее |
| Long Polling | 10,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 (с нуля до работающего кода):
| Технология | Время | Сложность кода | Подводных камней |
|---|---|---|---|
| SSE | 1 час | ★☆☆☆☆ | Прямолинейно, работает из коробки |
| WebSocket | 3 часа | ★★★☆☆ | Нужен роутинг событий, reconnect, heartbeat |
| Long Polling | 2 часа | ★★☆☆☆ | Проще 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);
};Подводные камни:
- Broadcast всем → O(N) на каждое сообщение. При 10k пользователей это 10k send().
- Решение: используйте Redis Pub/Sub для координации между серверами.
- Нет персистентности. Перезапуск сервера → все отключаются.
- Решение: храните сообщения в БД, при reconnect загружайте историю.
- Один сервер. Горизонтальное масштабирование без координации невозможно.
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
↓
Новый запрос
Таблица принятия решения
| Критерий | WebSocket | SSE | Long Polling |
|---|---|---|---|
| Latency | < 1ms | < 1ms | 50-200ms |
| Трафик | Минимум | Низкий | Высокий |
| Двусторонняя связь | ✅ | ❌ | ✅ |
| Простота реализации | ★★★☆☆ | ★☆☆☆☆ | ★★☆☆☆ |
| Нагрузка на сервер | Низкая | Низкая | Средняя |
| Совместимость | 95% | 98% | 100% |
| Работа через прокси | ⚠️ | ✅ | ✅ |
| Reconnect из коробки | ❌ | ✅ | ✅ |
| Масштабирование | Сложное | Среднее | Простое |
| Use case | Чаты | Dashboards | Notifications |
Гибридный подход: лучшее из всех миров
Реальная практика: Используйте несколько технологий в одном приложении.
Пример: 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— throughputreconnect_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 вот что я знаю точно:
-
SSE недооценён. В 80% кейсов "нужен WebSocket" на самом деле хватит SSE. Проще, надёжнее, меньше граблей.
-
WebSocket — это инфраструктура. Если решились, будьте готовы настраивать nginx, писать reconnect логику, мониторить heartbeat, координировать через Redis. Но когда нужна минимальная latency — альтернативы нет.
-
Long Polling жив. На мобильных в слабой сети (2G/3G) он работает стабильнее WebSocket. А для редких событий (раз в минуту) — самый экономный по ресурсам.
-
Начинайте с простого. Не делайте WebSocket "потому что модно". Сделайте обычный HTTP polling → если станет узким горлом → переходите на SSE → если и его не хватит → тогда WebSocket.
-
Тестируйте на мобильных. Desktop-браузеры держат WebSocket/SSE часами. Мобильный Safari закроет соединение через 30 секунд фона. Учитывайте это.
Следующий шаг: Выберите один подход, внедрите за выходные, измерьте метрики. Не пытайтесь сразу сделать "правильно" — сделайте работающее, потом оптимизируйте.
P.S. Если после статьи всё равно не знаете, что выбрать — начните с SSE. Серьёзно. Это как git commit в мире real-time: работает в 80% случаев, остальные 20% — исключения.
Полезные ссылки:
- FastAPI WebSocket Documentation
- SSE-Starlette Library
- MDN: Server-Sent Events
- WebSocket Protocol RFC 6455
Делитесь опытом
Я рассказал свой опыт с real-time в production. Теперь ваша очередь:
- Какую технологию используете вы?
- С какими граблями столкнулись?
- Что бы сделали иначе, зная то, что знаете сейчас?
Пишите в комментариях или в Telegram. Обсудим, сравним, посмеёмся над ошибками.
Нужна консультация по выбору real-time решения? Пишите на почту — разберу ваш кейс и дам честную рекомендацию. Без продаж, без воды, только практика.
Подписывайтесь на обновления в Telegram — пишу про Python, архитектуру и боль разработки. Без воды, только практика.

