Real-Time APIs · Real-Time Transports
WebSockets in Depth
Handshake, frames, heartbeats, close codes and message protocols.
A WebSocket starts as an HTTP request with Upgrade: websocket and becomes a persistent, full-duplex connection carrying text/binary frames. You must define your own application protocol on top (message types, IDs, acks, errors) — or use one (STOMP, graphql-ws, Socket.IO, MQTT over WS). Implement heartbeats (ping/pong) to detect dead connections through NATs and proxies, use close codes meaningfully, and authenticate during the handshake.
Common close codes
| Close code | Meaning |
|---|---|
| 1000 | Normal closure |
| 1001 | Going away (server shutdown, page navigation) |
| 1008 | Policy violation (e.g., auth failed) |
| 1009 | Message too big |
| 1011 | Internal server error |
| 4000–4999 | Application-defined |
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
server.on("upgrade", async (req, socket, head) => {
const user = await authenticate(req); // cookie or token in Sec-WebSocket-Protocol / first message
if (!user || !allowedOrigin(req.headers.origin)) return socket.destroy();
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, user));
});
wss.on("connection", (ws, user) => {
let alive = true;
ws.on("pong", () => (alive = true));
const hb = setInterval(() => { if (!alive) return ws.terminate(); alive = false; ws.ping(); }, 30_000);
ws.on("message", (raw) => handle(user, JSON.parse(raw.toString()))); // {type, id, payload}
ws.on("close", () => clearInterval(hb));
});
@app.websocket("/ws")
async def ws_endpoint(ws: WebSocket):
user = await authenticate_ws(ws) # session cookie / token
if user is None:
await ws.close(code=1008) # policy violation
return
await ws.accept()
try:
while True:
msg = await ws.receive_json()
await dispatch(user, msg, ws)
except WebSocketDisconnect:
await on_disconnect(user)
@Configuration
@EnableWebSocket
class WsConfig implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatHandler(), "/ws").setAllowedOrigins("https://app.example.com");
}
}
class ChatHandler extends TextWebSocketHandler {
@Override protected void handleTextMessage(WebSocketSession s, TextMessage m) { /* parse {type,id,payload} */ }
}
Common pitfalls
- No heartbeats (half-open connections linger).
- No Origin check (cross-site WebSocket hijacking).
- No message size limits.
Key points
- Define an application protocol
- Heartbeats + close codes
- Authenticate + check Origin at handshake
Further reading: RFC 6455: The WebSocket Protocol
Practice questions
What is cross-site WebSocket hijacking?
Answer
A malicious site opens a WebSocket to your server using the victim's cookies; since WebSockets aren't covered by CORS, the server must validate the Origin header and/or use token-based auth.
Why are heartbeats needed on WebSocket connections?
Answer
Network middleboxes can silently drop idle connections; ping/pong detects dead peers so resources are freed and clients reconnect.
Open in the app to mark lessons complete, take notes and track your progress.