When updating the order status, the client has to reload the page or poll the API every 5 seconds. This increases server load and degrades UX. A similar situation occurs on a financial platform where quotes change every second—WebSocket is overkill, but constant polling kills the database. We solve this problem with Server-Sent Events (SSE)—a one-way streaming technology from server to client over HTTP. Our experience shows: SSE reduces the number of requests by 10x and delivers events in <1 second.
Why SSE and not WebSocket?
| Criterion | SSE | WebSocket |
|---|---|---|
| Direction | One-way (server→client) | Bidirectional |
| HTTP compatibility | Native HTTP/1.1 | Requires upgrade |
| Auto-reconnection | Built-in | Needs implementation |
| Proxy compatibility | Works seamlessly | Often blocked |
| CORS | GET without preflight | Preflight for non-GET |
SSE is better suited for tasks where notifications and live updates matter. Client-to-server data sending is rare. Setting up SSE takes on average half the time compared to WebSocket. For example, for order status notifications SSE is ideal, while for chat WebSocket is better.
How does SSE scale across multiple servers?
In one project for a large e-commerce, we implemented SSE with distributed delivery via Redis Pub/Sub. The client achieved a reduction in order status update time from 5 minutes to 1 second. The system handles 10,000 concurrent connections without event loss. Scaling is achieved through a message broker: all servers subscribe to a common channel and publish events to all clients. We have used this pattern in over 30 projects in recent years.
Technical Implementation
SSE Format
Server response is text/event-stream with fields 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":"New order"}
event: order_update
data: {"orderId":"123","status":"shipped"}
id: msg_456
retry: 3000
: comment (ignored by client)
Server endpoint (Node.js + Express)
Full server endpoint code with heartbeat and client registry
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', // for Nginx: disable buffering
});
// Immediately send the first packet (bypass nginx buffering)
res.write(':ok\n\n');
// Add client to registry
const clientId = nanoid();
clients.set(clientId, { res, userId });
// Heartbeat every 15 seconds
const heartbeat = setInterval(() => {
res.write(': ping\n\n');
}, 15000);
req.on('close', () => {
clearInterval(heartbeat);
clients.delete(clientId);
});
});
// Send event to specific user
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 to all
function broadcast(event: string, data: object) {
clients.forEach(({ res }) => {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
}
Client using EventSource
Connection via EventSource with named event support:
const eventSource = new EventSource('/api/events', { withCredentials: true });
// Default events (no event name)
eventSource.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log('Message:', data);
};
// Named events
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);
});
// Error handling
eventSource.onerror = (e) => {
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed, automatically reconnecting...');
}
};
For more details about EventSource, refer to the official documentation: MDN Web Docs.
Scaling with Redis Pub/Sub
sub.subscribe('user:events', (message) => {
const { userId, event, data } = JSON.parse(message);
sendToUser(userId, event, data);
});
// From any service
await pub.publish('user:events', JSON.stringify({
userId: 'user_123',
event: 'payment_completed',
data: { amount: 5000 }
}));
Example named events
| Event | Data Format |
|---|---|
notification |
{"type":"success","message":"..."} |
order_update |
{"orderId":"123","status":"shipped"} |
payment_completed |
{"amount":5000,"currency":"USD"} |
Real case: SSE for a financial platform
For a startup providing real-time cryptocurrency data, we designed an SSE-based system. The requirement was to send quotes for 50+ currency pairs to thousands of users without delay and without burdening client databases. Solution: each user subscribes to desired pairs via a custom endpoint, and servers aggregate events via Redis Pub/Sub. Result: delivery latency under 200 ms even with 10,000 connections. This allowed us to abandon WebSocket, simplify infrastructure, and save on development. We guarantee stability of this scheme under high load.
How to ensure reliable reconnection?
SSE automatically reconnects, but it is important to configure a heartbeat. If the server does not send data for 30 seconds, the browser may close the connection. Therefore, we send an empty comment (: ping) every 15 seconds. This keeps the connection alive. Also, use the header X-Accel-Buffering: no for Nginx to prevent data buffering.
SSE limitations
- Server→client only — the client cannot send data via SSE (only new EventSource requests or separate API calls).
- Connection limit in HTTP/1.1 — browsers limit to 6 connections per domain. SSE consumes one. Solution: HTTP/2 (single multiplexed stream).
-
No IE support — use a polyfill
eventsourcefor older browsers.
What's included and typical mistakes
- Designing the event schema and data types.
- Implementing the server endpoint with authentication and heartbeat.
- Client integration (React hook, Angular service, or native EventSource).
- Load testing up to 10,000 connections.
- Documenting the protocol and code.
- Training the client's team.
Typical mistakes when implementing SSE:
- Forgetting Nginx buffering — missing the
X-Accel-Buffering: noheader. - Using SSE for bidirectional communication — WebSocket is more appropriate.
- Not setting a heartbeat — the connection may drop and not recover.
Timelines
SSE endpoint with authentication, heartbeat, Redis scaling: 3–5 days. With typed events, client-side hook (useSSE), and notification integration: 1–2 weeks.
Let's evaluate your project — contact us for a consultation. Over the years, we have implemented many SSE-based solutions, guaranteeing stability under high load. If you need to implement live updates, get a consultation today.







