При обновлении статуса заказа клиенту приходится перезагружать страницу или тянуть API каждые 5 секунд. Это увеличивает нагрузку на сервер и ухудшает UX. Аналогичная ситуация — на финансовой платформе, где котировки меняются каждую секунду, но WebSocket избыточен, а постоянные поллинги убивают базу данных. Мы решаем эту проблему с помощью Server-Sent Events (SSE) — технологии однонаправленной потоковой передачи данных от сервера к клиенту через HTTP. Наш опыт показывает: SSE снижает количество запросов в 10 раз и обеспечивает доставку событий за <1 с.
Почему SSE, а не WebSocket?
| Критерий | SSE | WebSocket |
|---|---|---|
| Направление | Одностороннее (сервер→клиент) | Двустороннее |
| HTTP-совместимость | Нативный HTTP/1.1 | Требует upgrade |
| Автопереподключение | Встроенное | Нужно реализовывать |
| Работа через прокси | Без проблем | Часто блокируется |
| CORS | GET без preflight | Preflight для не-GET |
SSE лучше подходит для задач, где важны уведомления и live-обновления. Отправка данных от клиента редка. Настройка SSE в среднем занимает в два раза меньше времени, чем WebSocket. Например, для уведомлений о статусе заказа SSE идеален, а для чата — WebSocket.
Как SSE масштабируется на несколько серверов?
В одном проекте для крупного e-commerce мы реализовали SSE с распределённой доставкой через Redis Pub/Sub. Клиент получил уменьшение времени обновления статуса заказа с 5 минут до 1 секунды. При этом система выдерживает 10 000 одновременных подключений без потери событий. Масштабирование достигается за счёт брокера сообщений: все серверы подписываются на общий канал и публикуют события для всех клиентов. Мы используем этот паттерн более чем в 30 проектах за последние годы.
Техническая реализация
Формат SSE
Ответ сервера — text/event-stream с полями data:, event:, id:, retry::
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"type":"notification","message":"Новый заказ"}
event: order_update
data: {"orderId":"123","status":"shipped"}
id: msg_456
retry: 3000
: comment (игнорируется клиентом)
Серверный endpoint (Node.js + Express)
Полный код серверного endpoint с heartbeat и реестром клиентов
app.get('/api/events', (req, res) => {
const userId = req.user.id;
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // для Nginx: отключить буферизацию
});
// Немедленно отправляем первый пакет (обход nginx буферизации)
res.write(':ok\n\n');
// Добавляем клиента в реестр
const clientId = nanoid();
clients.set(clientId, { res, userId });
// Heartbeat каждые 15 секунд
const heartbeat = setInterval(() => {
res.write(': ping\n\n');
}, 15000);
req.on('close', () => {
clearInterval(heartbeat);
clients.delete(clientId);
});
});
// Отправка события конкретному пользователю
function sendToUser(userId: string, event: string, data: object) {
clients.forEach(({ res, userId: uid }) => {
if (uid === userId) {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
});
}
// Broadcast всем
function broadcast(event: string, data: object) {
clients.forEach(({ res }) => {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
}
Клиент на EventSource
Подключение через EventSource с поддержкой именованных событий:
const eventSource = new EventSource('/api/events', { withCredentials: true });
// Дефолтные события (event: без имени)
eventSource.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log('Message:', data);
};
// Именованные события
eventSource.addEventListener('order_update', (e) => {
const order = JSON.parse(e.data);
updateOrderStatus(order.orderId, order.status);
});
eventSource.addEventListener('notification', (e) => {
showNotification(JSON.parse(e.data).message);
});
// Обработка ошибок
eventSource.onerror = (e) => {
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Соединение закрыто, автоматически переподключается...');
}
};
Подробнее об EventSource можно прочитать в официальной документации: MDN Web Docs.
Масштабирование с Redis Pub/Sub
sub.subscribe('user:events', (message) => {
const { userId, event, data } = JSON.parse(message);
sendToUser(userId, event, data);
});
// Из любого сервиса
await pub.publish('user:events', JSON.stringify({
userId: 'user_123',
event: 'payment_completed',
data: { amount: 5000 }
}));
Примеры именованных событий
| Событие | Формат данных |
|---|---|
notification |
{"type":"success","message":"..."} |
order_update |
{"orderId":"123","status":"shipped"} |
payment_completed |
{"amount":5000,"currency":"USD"} |
Реальный кейс: SSE для финансовой платформы
Для стартапа, предоставляющего данные о криптовалютах в реальном времени, мы спроектировали систему на SSE. Требовалось отправлять котировки 50+ валютных пар тысячам пользователей без задержек и без нагрузки на клиентские базы. Решение: каждый пользователь подписывается на нужные пары через кастомный endpoint, а серверы объединяют события по протоколу Redis Pub/Sub. В итоге — задержка доставки не более 200 мс даже при 10 000 подключений. Это позволило отказаться от WebSocket, упростить инфраструктуру и сэкономить на разработке. Гарантируем стабильность такой схемы под высокой нагрузкой.
Как обеспечить надежное переподключение?
SSE автоматически переподключается, но важно настроить heartbeat. Если сервер не отправляет данные 30 секунд, браузер может закрыть соединение. Поэтому мы отправляем пустой комментарий (: ping) каждые 15 секунд. Это поддерживает соединение живым. Кроме того, используйте заголовок X-Accel-Buffering: no для Nginx, чтобы данные не буферизировались.
Ограничения SSE
- Только сервер → клиент — клиент не может отправить данные через SSE (только новые EventSource-запросы или отдельный API-запрос).
- Лимит соединений в HTTP/1.1 — браузер ограничивает 6 соединений на домен. SSE занимает одно. Решение: HTTP/2 (один мультиплексируемый поток).
-
IE не поддерживает — полифилл
eventsourceдля старых браузеров.
Что входит в работу и типичные ошибки
- Проектирование схемы событий и типов данных.
- Реализация серверного endpoint с аутентификацией и heartbeat.
- Клиентская интеграция (React hook, Angular service или нативный EventSource).
- Нагрузочное тестирование до 10 000 подключений.
- Документирование протокола и кода.
- Обучение команды заказчика.
Типичные ошибки при внедрении SSE:
- Не учитывают буферизацию Nginx — забывают заголовок
X-Accel-Buffering: no. - Используют SSE для двусторонней связи — правильнее WebSocket.
- Не ставят heartbeat — соединение может разорваться и не восстановиться.
Сроки
SSE-эндпоинт с аутентификацией, heartbeat, Redis-scaling: 3–5 дней. С типизированными событиями, client-side хуком (useSSE), интеграцией уведомлений: 1–2 недели.
Оценим ваш проект — свяжитесь с нами для консультации. За последние годы мы реализовали множество решений на SSE, гарантируем стабильность под высокой нагрузкой. Если вам нужно внедрить live-обновления — получите консультацию сегодня.







