Server-Sent Events: The Simple Alternative to WebSockets
Learn when to use SSE instead of WebSockets for one-way real-time streaming, with Node.js code examples
When building real-time features like notifications, live dashboards, or activity feeds, most developers immediately reach for WebSockets. But there's a simpler alternative that works perfectly for one-way server-to-client communication: Server-Sent Events (SSE).
Why SSE?
WebSockets are powerful—they give you full-duplex, bidirectional communication over a single connection. But that power comes with complexity:
- Protocol upgrade required
- Special port considerations
- Connection management overhead
- Binary data handling
SSE solves this by staying strictly within HTTP. No upgrades, no special ports, no headache. Just plain old HTTP connections that stay open for streaming data from server to client.
When to Use SSE vs WebSockets
| Scenario | Use SSE | Use WebSocket |
|---|---|---|
| Server pushing notifications | ✓ | |
| Live dashboard updates | ✓ | |
| Activity feeds | ✓ | |
| Chat applications | ✓ | |
| Multiplayer games | ✓ | |
| Real-time collaboration | ✓ |
Use SSE when: You only need the server to send data to the client. Notifications, dashboards, live feeds, progress bars, and log streaming are perfect use cases.
Use WebSockets when: You need bidirectional communication where both client and server send messages freely.
How SSE Works
SSE uses a simple text-based wire format. The server keeps an HTTP connection open and writes events as plain text:
data: {"message": "hello"}
data: {"message": "world"}Each event can have:
data- The payloadevent- Optional event type (for filtering)id- Event ID for resumption on reconnectretry- Reconnection timeout in milliseconds
Server Implementation (Node.js/Express)
Here's a simple SSE endpoint:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
const clients = [];
app.get('/events', (req, res) => {
const headers = {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
};
res.writeHead(200, headers);
const clientId = Date.now();
const newClient = {
id: clientId,
res,
};
clients.push(newClient);
req.on('close', () => {
console.log(`${clientId} Connection closed`);
clients = clients.filter(c => c.id !== clientId);
});
});
// Endpoint to broadcast events to all connected clients
app.post('/broadcast', express.json(), (req, res) => {
const { message } = req.body;
clients.forEach(client => {
client.res.write(`data: ${JSON.stringify({ message })}\n\n`);
});
res.json({ success: true });
});
app.listen(3000, () => {
console.log('SSE server running on port 3000');
});Client Implementation
The browser provides a native EventSource API:
const eventSource = new EventSource('http://localhost:3000/events');
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data.message);
});
eventSource.addEventListener('open', () => {
console.log('Connected to SSE stream');
});
eventSource.addEventListener('error', (event) => {
if (event.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
});
// Clean up when done
// eventSource.close();Key Features
Automatic Reconnection
The browser automatically reconnects if the connection drops. No extra code needed.
Event IDs for Resumption
// Server sends events with IDs
client.res.write(`id: 1\ndata: {"step": 1}\n\n`);
client.res.write(`id: 2\ndata: {"step": 2}\n\n`);
// On reconnect, browser sends Last-Event-ID header
// Server can resume from where client left offEvent Types
// Server: send typed events
client.res.write(`event: notification\ndata: {"type": "alert"}\n\n`);
client.res.write(`event: update\ndata: {"status": "complete"}\n\n`);
// Client: listen for specific types
eventSource.addEventListener('notification', (e) => {
console.log('Notification:', e.data);
});Production Considerations
Scaling
SSE connections are stateful. For horizontal scaling:
- Use a message broker (Redis pub/sub, Kafka, NATS)
- Each server subscribes to the broker and relays to connected clients
- Consider sticky sessions or proxy-level routing
Security
- Always use HTTPS
- Authenticate at connection time (query params, headers)
- Implement rate limiting per connection
- Set reasonable retry intervals
Monitoring
Track:
- Active connection count
- Connection open/close rates
- Message throughput and latency
- Error rates
Conclusion
Server-Sent Events are the forgotten gem of real-time web development. When you only need server-to-client streaming, SSE offers:
- Simpler implementation than WebSockets
- Works through firewalls, proxies, and load balancers
- Built-in reconnection and event resumption
- Plain text debugging
Next time you need real-time updates, ask yourself: do I really need bidirectional communication? If not, SSE might be the simpler solution you need.