Walk through the WebSocket protocol handshake, and when do you actually need a persistent connection?
A WebSocket starts life as a normal HTTP request and only becomes something else after both sides agree to stop speaking HTTP — the handshake tells you exactly what you're trading away.
LEARN BY DOINGPut your intuition to work3 steps
Walk through the WebSocket protocol handshake, and when do you actually need a persistent connection?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#The upgrade handshake
A WebSocket connection begins as a completely ordinary HTTP GET request. The client asks the server to switch protocols on the same TCP connection instead of opening a new one:
GET /chat HTTP/1.1Host: api.example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Sec-WebSocket-Version: 13Origin: https://app.example.com
Sec-WebSocket-Key is 16 random bytes, base64-encoded, generated fresh by the client for this handshake. It is not a security credential — its job is to prove that the server receiving the request actually understands the WebSocket protocol (rather than being, say, a caching proxy that blindly forwarded the Upgrade header) and to give browsers a defense against a misbehaving proxy replaying a captured HTTP response as if it were a real WebSocket handshake.
The server proves it understood by combining that key with a fixed, protocol-defined GUID, hashing it, and sending the result back:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
The exact computation is: base64(sha1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")). That GUID is hard-coded into the spec (RFC 6455) — every WebSocket server on earth uses the same literal string. It's there purely so both sides prove they're actually implementing the WebSocket spec, not so it's secret.
const crypto = require('crypto');function computeAcceptValue(clientKey) {const MAGIC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';return crypto.createHash('sha1').update(clientKey + MAGIC_GUID).digest('base64');}console.log(computeAcceptValue('dGhlIHNhbXBsZSBub25jZQ=='));// s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Once the 101 Switching Protocols response lands, the TCP connection stops speaking HTTP entirely. Both sides now exchange WebSocket frames — a small binary format carrying an opcode (text, binary, ping, pong, close), a payload length, and (for client-to-server frames only) a masking key applied to the payload, which exists to stop malicious web pages from crafting byte sequences that could be misread as valid requests by naive proxies sitting between browser and server. From this point the connection is full-duplex: either side can send a frame at any moment, with no request/response pairing required.
#A minimal server and client
// server.js — Node, using the "ws" packageconst { WebSocketServer } = require('ws');const wss = new WebSocketServer({ port: 8080 });wss.on('connection', (socket, request) => {console.log('Client connected from', request.socket.remoteAddress);socket.on('message', (data) => {const message = JSON.parse(data.toString());if (message.type === 'subscribe') {socket.send(JSON.stringify({ type: 'ack', channel: message.channel }));}});const heartbeat = setInterval(() => {if (socket.readyState === socket.OPEN) socket.ping();}, 30000);socket.on('close', (code, reason) => {clearInterval(heartbeat);console.log(`Connection closed: ${code} ${reason}`);});});
// client.js — runs in the browser, no library neededconst socket = new WebSocket('wss://api.example.com/chat');socket.addEventListener('open', () => {socket.send(JSON.stringify({ type: 'subscribe', channel: 'orders:42' }));});socket.addEventListener('message', (event) => {const payload = JSON.parse(event.data);console.log('Received:', payload);});socket.addEventListener('close', (event) => {console.log(`Closed: ${event.code} ${event.reason}`);});
Note the ping/pong control frames in the server code — they exist because a TCP connection sitting idle looks identical, from the application's point of view, whether the peer is fine and just quiet, or the peer's machine crashed and the network never told you. A heartbeat is how you detect the second case before your load balancer or a stateful in-memory subscription list quietly accumulates dead connections.
#When you actually need this vs when you don't
WebSockets earn their cost when the server needs to push data the client didn't ask for, at a frequency or latency where opening a new HTTP request per message would be wasteful or too slow: live chat, collaborative editing (multiple cursors typing into the same document), multiplayer game state, live trading tickers, or dashboards that need sub-second updates. In those cases the alternative — the client polling every second "anything new?" — either wastes requests when nothing changed or adds up to a second of latency when something did.
You very often don't need one. If updates are occasional (once every 30 seconds or slower), plain polling is simpler and stateless. If the server needs to push but the client never talks back, Server-Sent Events (a single long-lived HTTP response streamed as text/event-stream) is usually the better tool: it's plain HTTP so it multiplexes cleanly over HTTP/2, it has automatic reconnection built into the browser's EventSource API, and it doesn't need any special handling from load balancers or corporate proxies the way a raw TCP upgrade sometimes does. And if the interaction is fundamentally request/response — "fetch me this resource" — regular REST over HTTP is simpler, cacheable, and statelessly scalable in a way a WebSocket connection is not.
That last point is the real cost of choosing WebSockets: they make your server stateful. Once a client is attached to a specific server process by an open socket, a request for that client's data has to be routed back to that process — you can't just round-robin behind a stateless load balancer the way you can with HTTP. Scaling out usually means sticky sessions plus a pub/sub backplane (Redis, Kafka, a dedicated WebSocket-fanout service) so that a message published on any server instance reaches the right subscriber regardless of which instance holds their socket. Every open connection also holds a file descriptor and some memory on the server for as long as it's alive, so a service with a few hundred thousand idle connections has a very different operational profile than one serving stateless request/response traffic.