Real-time web features are no longer exotic — live dashboards, collaborative tools, AI-generated streaming responses, and notifications are expected in modern applications. The question is not whether to do real-time, but which transport to use. The answer depends on one thing: does data flow in one direction or two?
What changed in 2026
- AI streaming (LLM token-by-token output) normalised SSE — the OpenAI, Anthropic, and Google Gemini APIs all stream responses via SSE, and every frontend framework now has first-class SSE support.
- HTTP/2 became the universal default across cloud providers and CDNs, eliminating the 6-connection-per-domain SSE limit that plagued HTTP/1.1 deployments.
- WebSocket support in edge runtimes (Cloudflare Workers, Deno Deploy, Vercel Edge Functions) matured, making truly distributed WebSocket deployments feasible without a dedicated server tier.
- Long polling is effectively retired — where it was once the fallback, HTTP/2 SSE now fulfils the same need with lower server overhead and better client tooling.
Core comparison
| Dimension |
SSE |
WebSockets |
| Direction |
Server → client only |
Bidirectional |
| Protocol |
HTTP (plain) |
WS/WSS (upgrade) |
| Auto-reconnect |
Built-in |
Manual |
| Proxy compatibility |
Full (HTTP) |
Requires WS-aware proxy |
| Browser limit (HTTP/1.1) |
6 per domain |
Unlimited |
| Browser limit (HTTP/2) |
Unlimited (streams) |
Unlimited |
| Headers on reconnect |
Sent (with Last-Event-ID) |
Not applicable |
| Message ordering |
Guaranteed |
Guaranteed |
| Infrastructure complexity |
Low |
Medium-high |
When SSE is the right choice
SSE is an HTTP streaming response — the server writes data: ... lines to a long-lived HTTP connection. The browser's EventSource API handles reconnection, Last-Event-ID resumption, and event parsing automatically.
// Client — zero dependencies
const es = new EventSource("/api/stream/orders");
es.onmessage = (e) => {
const event = JSON.parse(e.data);
renderOrder(event);
};
es.addEventListener("order_shipped", (e) => {
showShippingAlert(JSON.parse(e.data));
});
# Server (FastAPI)
from fastapi.responses import StreamingResponse
import asyncio, json
async def event_stream(order_id: str):
while True:
update = await get_next_update(order_id)
yield f"event: order_shipped\ndata: {json.dumps(update)}\n\n"
await asyncio.sleep(0.1)
@app.get("/api/stream/orders/{order_id}")
async def stream_order(order_id: str):
return StreamingResponse(event_stream(order_id),
media_type="text/event-stream")
Use SSE for: live dashboards, notifications, AI token streaming, order status updates, activity feeds — anything where data flows server-to-client.
When WebSockets are necessary
WebSockets provide a full-duplex channel. Both sides can send at any time without a request-response cycle. This is essential for:
- Chat applications — messages flow both directions independently.
- Collaborative editing — cursor positions and document patches from every client, broadcast to all.
- Multiplayer / real-time gaming — sub-100ms bidirectional state synchronisation.
- Live cursor tracking / whiteboarding — high-frequency position updates from the client.
// Client
const ws = new WebSocket("wss://api.example.com/collab/doc/123");
ws.onmessage = (e) => applyPatch(JSON.parse(e.data));
ws.onopen = () => ws.send(JSON.stringify({ type: "join", userId: "u_abc" }));
// Send a patch from the client (bidirectional!)
function sendPatch(patch) {
ws.send(JSON.stringify({ type: "patch", payload: patch }));
}
Infrastructure implications
SSE: runs over port 443 HTTPS, traverses every proxy and load balancer that handles HTTP — no special configuration. A standard Nginx/ALB setup works out of the box. No sticky sessions needed (each SSE connection is stateless at the proxy level).
WebSockets: the HTTP upgrade handshake must succeed end-to-end. Configure:
# Nginx WebSocket proxy config
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; # Keep alive for long-lived connections
}
For horizontal scaling, WebSocket connections are stateful (messages go to specific server instances). You need either sticky sessions (session affinity at the load balancer) or a pub/sub fan-out layer (Redis Pub/Sub, Ably, Pusher) so any server instance can receive and forward messages to the right connection.
AI streaming: the SSE standard
Every major LLM API streams responses as SSE:
# Anthropic SDK — streaming via SSE under the hood
import anthropic
client = anthropic.Anthropic()
with client.messages.stream(
model="claude-opus-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Explain indexing."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
Proxy this directly to your frontend — SSE is the correct transport for all token streaming scenarios.
How to pick
- Is data flowing only server-to-client? → SSE. No exceptions for simplicity.
- Does the client need to send messages outside of a request? → WebSockets.
- Are you building a notification system, live feed, or AI output stream? → SSE.
- Are you building chat, collaborative editing, or multiplayer? → WebSockets.
- Are you running on a serverless edge runtime? → SSE is simpler; WebSocket support exists but varies by provider.
Common mistakes
Using WebSockets for one-way streaming. The operational overhead (proxy config, sticky sessions, manual reconnection) is unjustified when SSE would work with zero special infrastructure.
Not handling SSE reconnection state. Use Last-Event-ID to resume from where the client left off after a disconnect — send it with every event and handle it on the server.
No heartbeat on WebSockets. Idle WebSocket connections are killed by proxies (typically 60–120s timeout). Send a periodic ping frame to keep the connection alive.
Unbounded connection count per server. A Node.js server holding 10,000 WebSocket connections is manageable; 100,000 is not. Plan for horizontal scaling or a managed service (Pusher, Ably, Soketi) before you need it.
What to skip
- Long polling in 2026 — it's a workaround from the HTTP/1.1 era. SSE over HTTP/2 is universally available and superior in every metric.
- Socket.io unless you need its specific features — it adds a significant dependency and hides the transport abstraction. Native WebSocket + SSE is simpler and more portable in 2026.
- Polling (setInterval + fetch) for data that changes frequently — it adds unnecessary load on your server and latency for the client.
FAQ
Can I mix SSE and WebSockets in the same app?
Yes, and many apps do — SSE for server-push notifications, WebSockets for the collaborative document editor. Use the right transport for each feature.
Does SSE work with Next.js / Vercel?
Yes. Vercel's Edge Runtime supports SSE via Response streaming. Serverless functions with short timeouts can't hold long SSE connections, so use Vercel's Streaming API or a dedicated WebSocket service for persistent connections.
What happens when an SSE connection drops?
The browser's EventSource automatically reconnects after ~3 seconds by default. The server receives the Last-Event-ID header if you sent event IDs, so it can replay missed events.
How do I scale WebSockets across multiple servers?
Use Redis Pub/Sub as a message bus: when server A receives a message for a connection on server B, it publishes to Redis; server B subscribes and forwards to the connection. Socket.io, Ably, and Pusher abstract this for you.
Where to go next
See Event-driven architecture in 2026 for the backend patterns that feed real-time data streams, and API rate limiting in 2026 for protecting high-frequency WebSocket or SSE endpoints.