
Server-Sent Events (SSE): The Simple Alternative to WebSockets
Abhay Vachhani
Developer
SSE Implementation Checklist
- Content-Type: text/event-stream
- Redis Pub/Sub for broadcasting
- Heartbeat (30s) prevents timeouts
- Nginx 'proxy_buffering off'
Every developer reaching for real-time features instinctively goes for WebSockets. But WebSockets are a completely different protocol (WS) that requires custom load balancing and state management. For 80% of use cases—like status updates, AI completions, or news feeds—Server-Sent Events (SSE) are a better choice. They run over standard HTTP and are natively supported by browsers via EventSource.
1. The Messaging Anatomy: Protocol Deep-Dive
SSE isn't just a text stream; it's a structured protocol. To provide a professional experience, you should use all three core fields:
- id: A unique identifier for the message. If the client reconnects, it sends the `Last-Event-ID` header so you can resume from that exact point.
- event: Allows you to categorize messages. The frontend can listen to specific event types (e.g., `source.addEventListener('order-update', ...)`).
- retry: Tells the browser exactly how many milliseconds to wait before attempting a reconnection.
2. Distributed SSE: The Redis Pattern
If you have multiple API servers, how do you send an update to a user connected to Server A from logic running on Server B? The Solution: Use **Redis Pub/Sub**. All your SSE instances subscribe to a Redis channel. When an update happens, you publish it once to Redis, and all servers push it to their respective connected clients.
// Scaling SSE with Redis
sub.subscribe('global-updates', (message) => {
// Push to all locally connected clients
clients.forEach(res => {
res.write(`event: notification\n`);
res.write(`data: ${message}\n\n`);
});
});
3. The 6-Connection Limit and Sharding
Under HTTP/1.1, browsers limit you to 6 concurrent open connections per domain. If a user opens 7 tabs of your dashboard, the 7th SSE connection will hang forever.
- HTTP/2: The real fix. HTTP/2 multiplexes connections, effectively removing this limit for SSE.
- Domain Sharding: If stuck on HTTP/1.1, use subdomains like `events1.yoursite.com` and `events2.yoursite.com` to bypass the per-domain cap.
4. Reliability: Heartbeats and Keep-Alive
Intermediate proxies (like Nginx or Cloudflare) often drop "silent" connections after 30-60 seconds. To keep the pipe open, implement a **Heartbeat**. Every 15-30 seconds, send a comment line (starting with `:`) which the browser ignores but the proxy sees as active traffic.
// Heartbeat Implementation
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);
5. Pro Pattern: The Broadcast Controller
Managing a list of active `res` objects manually is error-prone. Build a central BroadcastManager that handles connection lifecycle, automatic cleanup on disconnect, and targeted messaging (sending updates to a specific `userId`).
Conclusion
Real-time updates don't have to be complicated. By choosing Server-Sent Events for one-way communication and augmenting them with Redis for scale, you achieve high performance without the weight of WebSockets. Sometimes, the simplest HTTP-native tool is the most robust one in your stack.
FAQs
When should I use SSE instead of WebSockets?
Use SSE when you only need one-way updates (Server to Client), such as stock tickers, news feeds, or progress bars. Use WebSockets if you need bidirectional, high-frequency interaction like a chat app.
Does SSE handle reconnections automatically?
Yes! The browser's native EventSource API automatically attempts to reconnect if the connection is dropped, making it much simpler to implement than custom heartbeat logic.
What are the main limitations of SSE?
SSE only supports UTF-8 text (no binary data), and browsers have a limit on the number of maximum concurrent open connections per domain (usually 6).