Real-time синхронизация без WebSocket: 99.94% надёжности
Три месяца назад наш syncs валился у каждого 35-го пользователя. После переезда с WebSocket на long-polling поверх HTTP/2 — у каждого 1700-го. Рассказываем что поменяли.
Проблема
В январе мы заметили странность: при достижении ~150 одновременных юзеров реконнекты WebSocket'ов начинали лавинообразно расти. На графиках Grafana — каждые 15-20 минут пик в 300-400 reconnect-ивентов в секунду. Юзеры жаловались: "задачи не появляются", "поставил статус, в соседнем устройстве не подтянулось".
Поначалу мы думали что виноваты конкретные ISP с агрессивным NAT-timeout. Подкрутили keepalive_interval до 25 секунд (стандартно 45) — стало чуть лучше, но проблема осталась.
Что мы попробовали (и не помогло)
- Уменьшение keepalive до 15 сек — снизило reconnect'ы на 12%, но утроило bandwidth на ping-pong'и
- Bidirectional ping с серверной стороны — не помогло, юзеры за corporate proxy всё равно отваливались
- STOMP вместо нативного WS — переусложнило и не починило root cause
Проблема оказалась глубже: WebSocket-соединения по-разному ведут себя через мобильные сети, corporate firewall'ы и Wi-Fi на нестабильном CGN'е оператора. Стандартизировать поведение на клиенте — невозможно.
Решение: long-polling поверх HTTP/2 streams
Идея простая. Клиент держит ОДИН долгоживущий HTTP/2 stream. Сервер шлёт события в стрим как chunks. Клиент при разрыве переподключается с курсором (last-seen event ID), сервер досылает пропущенное.
POST /api/v1/sync/stream
Content-Type: application/x-ndjson
X-Cursor: v_2026_05_18_141832
← {"type": "task.created", "id": "tsk_8jK", "data": {...}}
← {"type": "task.updated", "id": "tsk_9Lm", "patch": {...}}
← {"type": "heartbeat", "ts": 1747562340}
Что нам это даёт:
- HTTP/2 multiplexing — один TCP-коннект на несколько streams. На мобиле работает значительно стабильнее.
- Corporate proxies нативно умеют HTTP/2. WebSocket-upgrade некоторые рубят.
- Если стрим оборвался — переподключение тривиально: указали cursor, получили missed events.
- Backpressure встроено в HTTP/2 (через flow control windows).
Цифры до/после
Замеряли на одной и той же когорте юзеров (150 человек, mix iOS/Android/web):
- Reconnect rate: 2.8% → 0.06% (×46 улучшение)
- Event delivery p99 latency: 340 ms → 180 ms
- Bandwidth на keepalive: 4.2 kB/min → 0.8 kB/min
- Среднее время recovery после потери сети: 8 сек → 1.5 сек
Сюрпризы
Один из неожиданных бонусов — стало проще дебажить. WebSocket-фреймы в DevTools плохо читаются. NDJSON-стрим — просто открыл Network tab и видишь весь event-flow глазами.
Также упростилась интеграция с CDN. WebSocket-upgrade поддерживают не все CDN-провайдеры (или поддерживают за отдельные деньги). Long-polling HTTP/2 проходит через любой современный CDN без хаков.
Что дальше
Сейчас тестируем добавление HTTP/3 (QUIC) для мобильных клиентов — экспериментально показывает на 20-30% меньшую latency на 4G. К следующему мажорному релизу планируем дефолтнуть HTTP/3 на мобиле где доступно, fallback на HTTP/2 везде иначе.
— Команда инженеров FocusWork