HTTP is request–response: the browser asks, the server answers. For chat, live dashboards, notifications, multiplayer games or collaborative editors, the server needs to push data the moment something happens. WebSockets provide a persistent, two-way channel between browser and server over a single TCP connection.
How it works
- The browser sends a normal HTTP request with upgrade headers:
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
- The server agrees:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- The connection stays open. Both sides send frames (text or binary) whenever they want, with very little overhead per message.
In the browser:
const ws = new WebSocket('wss://example.com/ws');
ws.onmessage = (e) => console.log('received', e.data);
ws.onopen = () => ws.send(JSON.stringify({ type: 'subscribe', room: 'general' }));
Use wss:// (WebSocket over TLS) in production — like HTTPS.
WebSockets vs alternatives
| Technique | Direction | Good for |
|---|---|---|
| Polling | Client asks repeatedly | Simple, infrequent updates |
| Long polling | Server holds request until data | Legacy real-time |
| Server-Sent Events (SSE) | Server → client only, over HTTP | Notifications, live feeds, streaming AI responses |
| WebSockets | Both ways | Chat, games, collaboration, trading |
| WebTransport | Both ways over HTTP/3 | Emerging, low-latency streams |
If the server only needs to push updates, SSE is often simpler: plain HTTP, automatic reconnection, works through most proxies. Choose WebSockets when the client also sends frequent messages.
Servers and libraries
- Node.js:
ws(minimal), Socket.IO (rooms, reconnection, fallbacks — uses its own protocol on top), uWebSockets.js. - Python: ASGI frameworks (FastAPI/Starlette, Django Channels) with Uvicorn or Daphne.
- Go:
gorilla/websocket,nhooyr.io/websocket. - PHP: Laravel Reverb, Ratchet, Swoole/OpenSwoole.
Behind proxies and load balancers
Proxies must pass the upgrade headers and allow long-lived connections:
- Nginx —
proxy_http_version 1.1,Upgrade/Connectionheaders, longerproxy_read_timeout. See WebSockets through Nginx. - Caddy — works automatically.
- CDNs — most support WebSockets but may close idle connections after a timeout; send periodic pings.
Keeping connections alive
Idle connections get dropped by NATs, proxies and mobile networks. Send heartbeat pings every 20–60 seconds, and reconnect with exponential backoff on the client when the connection closes.
Scaling to several servers
A WebSocket stays connected to one server. When user A on server 1 sends a message to user B on server 2, the servers need a way to share it:
- A pub/sub backbone — Redis pub/sub or streams, NATS — that every server subscribes to. Socket.IO’s Redis adapter does exactly this. See Redis pub/sub.
- Sticky sessions at the load balancer if your protocol needs a client to keep hitting the same server (e.g. Socket.IO’s polling fallback).
- Connection limits — each socket is an open file descriptor; raise
ulimit -nand Nginxworker_connectionsfor many thousands of clients. See ulimit and open files.
Security
- Authenticate during the handshake (session cookie or a token), not after connecting.
- Check the
Originheader on the server — browsers don’t apply CORS to WebSockets, so cross-site WebSocket hijacking is possible without an origin check. - Validate every message like any untrusted input; enforce size limits and rate limits.
- Always use
wss://.
Hosting considerations
WebSocket servers hold many idle connections, so memory per connection and file descriptor limits matter more than CPU. Long-lived connections also mean deployments need care — drain connections gracefully and let clients reconnect.
A VPS is well suited: no execution time limits, full control over timeouts and kernel limits. Choose a location near your users, since every real-time message pays the network latency — see choosing a VPS location.