WebSocket vs HTTP: When You Actually Need a Persistent Connection
WebSocket gets reached for whenever real-time appears in requirements. Often, Server-Sent Events or polling is the right choice. Learn the tradeoffs, the protocol details, and the operational gotchas.
Real-time features get WebSocket for the same reason webhooks get cron — it's the obvious tool, but it's often the wrong tool. Server-Sent Events handle most "server pushes update" scenarios with less complexity. Long polling can beat both for low-frequency updates. Knowing when WebSocket is actually justified saves a lot of operational pain.
The four real-time patterns
1. Polling
Client makes a regular HTTP request every N seconds asking "anything new?". Server responds with either new data or empty.
- Simple. Works with any HTTP infrastructure.
- Wasteful — most requests get no new data.
- Latency = polling interval / 2 on average.
2. Long polling
Client opens a request; server holds it open until new data is available, then responds. Client immediately reopens.
- Lower latency than polling.
- Works through proxies and firewalls.
- One TCP connection per client at any time.
- HTTP timeout interactions can be annoying.
3. Server-Sent Events (SSE)
Server-to-client streaming over a single HTTP connection. Server sends events; client receives them via the EventSource API.
- Simpler than WebSocket.
- One-way (server-to-client only).
- Works through HTTP infrastructure.
- Auto-reconnects on disconnect.
- Native browser support.
4. WebSocket
Full-duplex persistent connection over a single TCP connection. Either side can send messages anytime.
- True bidirectional.
- Lowest latency.
- Operational complexity is highest.
- Doesn't reuse HTTP semantics (caching, status codes, etc.).
The decision matrix
Use Polling for
- Updates needed every minute or less frequently
- Simplest possible architecture
- Mobile apps with battery concerns
- Updates that arrive bursty rather than steadily
Use SSE for
- Server-to-client streams (notifications, live feeds)
- Stock tickers, sports scores, log streams
- Real-time updates with no client → server traffic
- Existing HTTP-based infrastructure
Use WebSocket for
- Bidirectional real-time (chat, gaming, collaborative editing)
- Sub-second latency required both ways
- High-frequency client interactions
- Multiplayer interactions
Use Long Polling for
- Restrictive corporate proxies
- Legacy infrastructure that breaks SSE/WS
- Updates expected within 30+ seconds
- Quick to implement without specialized tooling
WebSocket protocol details
A WebSocket connection starts with an HTTP "upgrade" handshake:
# Client request
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
# Server response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=After 101, the connection becomes a WebSocket. From then on, messages are framed as either text (JSON is common) or binary.
Operational complexity
Load balancing
WebSockets are sticky to a single server. Round-robin load balancers don't work — once connected, you must stay on the same backend for the duration. Configure your LB for sticky sessions or use a layer 4 (TCP) load balancer.
Scaling out
Sending a message to all connected clients requires fan-out. With one server, easy. With many servers each holding subsets of connections, you need a pub/sub infrastructure (Redis, Kafka, NATS) to broadcast across the fleet.
Connection limits
Each WebSocket holds an open file descriptor and TCP connection. Default OS limits often cap at 1024 per process. High-concurrency servers need tuned ulimit, kernel parameters, and careful memory management. Tools like Erlang/Elixir, Go, and Rust handle this gracefully; Node.js and Python struggle past ~10k connections per process.
Authentication
WebSocket doesn't have a standard auth mechanism. Common patterns:
- Cookie auth (works because the upgrade is HTTP).
- Token in URL query parameter (visible in logs — not great).
- Token via first message after connect.
- Subprotocol header carrying token.
Idle timeouts
SSE: the underused alternative
For unidirectional server-to-client real-time, SSE is dramatically simpler:
# Client
const events = new EventSource('/stream');
events.onmessage = (e) => console.log(e.data);
# Server (Express)
app.get('/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
setInterval(() => {
res.write(`data: ${JSON.stringify(getUpdate())}
`);
}, 1000);
});SSE benefits:
- Native browser auto-reconnect with last-event-id.
- Goes through HTTP infrastructure.
- HTTP/2 multiplexing means many SSE connections share one TCP connection — connection limits less of an issue.
- Same auth as regular HTTP.
SSE limitations:
- One-way only. Client-to-server still needs separate HTTP requests.
- Browser limits ~6 concurrent connections per origin (HTTP/1.1) — HTTP/2 fixes this.
- Less polished tooling than WebSocket.
HTTP/2 and HTTP/3 considerations
HTTP/2 multiplexing makes long polling and SSE much more efficient than under HTTP/1.1. Many WebSocket use cases (one-way streams) are simpler over HTTP/2 SSE.
HTTP/3 (QUIC) has built-in unreliable streams, making certain real-time patterns (live audio, game state) easier without WebSocket.
Common mistakes
- WebSocket for "real-time" that's actually periodic. Stock prices update every second? SSE handles it. Polling at 5-second intervals handles it.
- Forgetting heartbeats. Connections die silently after idle timeouts.
- No reconnection logic. Networks drop. Always plan for reconnect with backoff and resume.
- Using WebSocket without sticky sessions behind a load balancer. Random disconnects.
- Sending huge messages. WebSocket frames have no built-in compression. Use the permessage-deflate extension or compress payloads.
- Auth tokens in URL parameters. Logged everywhere. Use auth via subprotocol or first message.
Key Takeaways
- WebSocket is bidirectional, low-latency, full-duplex — best for chat, gaming, collaborative editing.
- Server-Sent Events (SSE) covers most server-to-client real-time scenarios with HTTP simplicity.
- Long polling and regular polling are valid for low-frequency updates; don't reach for WebSocket reflexively.
- WebSocket operational complexity: sticky load balancing, fan-out, connection limits, idle timeouts. Plan for these.
- HTTP/2 and HTTP/3 narrow the gap — polling and SSE are increasingly viable replacements for WebSocket.