При обновлении статуса заказа клиенту приходится перезагружать страницу или тянуть 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-оновлення — отримайте консультацію.







