WebSocket

WebSocket is a protocol that gives a browser (or any client) and a server a two-way connection that stays open. Either side can send a message whenever it wants, without waiting to be asked. It is what sits behind chat apps, live scoreboards, multiplayer games and collaborative editors. This lesson shows why it exists, how a normal HTTP request turns into a WebSocket, what the messages look like on the wire, and what to watch out for in a real project.

The Problem: Plain HTTP Cannot Push

As you saw in HTTP & HTTPS, the client always speaks first. A server has no way to tell a plain HTTP client "something just happened". So developers invented workarounds to get live data, and each one has a cost:

  • Polling. The client asks "anything new?" every few seconds. It is simple, but most answers are empty, every request carries full headers and cookies, and news can arrive up to one interval late.
  • Long polling. The client asks, and the server holds the request open until it has something to say (or a timeout hits). The client then immediately asks again. News arrives faster, but the server keeps many requests waiting and every message still costs a new request and response.
  • Server-Sent Events (SSE). The browser opens one HTTP response that never ends, and the server writes events into it. It works well, but data only flows from the server to the client, and only as text.
Polling compared with one WebSocket connection Two timelines. In the first, the client sends six requests to the server one after another and the server answers each; five answers are empty and only one carries data. In the second, the client and server complete one HTTP upgrade and then exchange five messages in both directions, each sent only when there is something to say. Polling: the client keeps asking time goes left to right Client Server empty empty empty data empty empty Most answers are empty, yet each one still costs a full request and response. WebSocket: one connection, messages in either direction time goes left to right Client Server HTTP upgrade push send push push send After one upgrade request, either side can send at any moment.
Polling pays for a round trip every time, even when nothing changed. WebSocket pays once, then each message travels only when there is something to say.

What Is WebSocket?

WebSocket (standardised as RFC 6455, with a matching browser API) solves this by changing the rules after the first request:

  • Full-duplex. Both sides can send at the same time, in any order.
  • One long-lived connection. It runs on a single TCP connection that stays open for minutes, hours or days, so there is no new connection per message.
  • Messages, not requests. After the handshake there are no methods, status codes or repeated headers. Each message carries only a few bytes of framing.
  • Two URL schemes. ws:// is unencrypted and typically uses port 80. wss:// runs over TLS, just like HTTPS, and typically uses port 443. Use wss:// on any real site.
  • It starts life as HTTP. The first thing on the wire is an ordinary HTTP request that asks the server to switch protocols. That is why WebSocket can share ports 80 and 443 with a normal website.

The Opening Handshake

The client sends an HTTP/1.1 GET request with a few special headers. If the server agrees, it answers 101 Switching Protocols. From then on the same TCP connection no longer speaks HTTP.

GET /chat HTTP/1.1
Host: example.com
Origin: https://example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
The WebSocket opening handshake The browser and the server first open a connection, using TLS for wss. The browser then sends an HTTP GET request with Upgrade, Connection, Sec-WebSocket-Key and Sec-WebSocket-Version headers. The server answers 101 Switching Protocols with a Sec-WebSocket-Accept header. After that, WebSocket frames flow in both directions on the same connection. Browser (client) Web server 1. Connection opened (TCP, plus TLS for wss://) 2. Upgrade request (still HTTP) GET /chat HTTP/1.1 Host: example.com Origin: https://example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 3. The server agrees HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 4. WebSocket frames in both directions the same connection, no more HTTP The key and accept values are the example pair from RFC 6455. A browser generates a new random key for every connection.
The upgrade is a normal HTTP exchange. Once the server answers 101, both sides stop speaking HTTP on that connection.

What each header is for:

HeaderPurpose
Upgrade: websocket and Connection: Upgrade"Please switch this connection to the WebSocket protocol."
Sec-WebSocket-KeyA random 16-byte value, base64-encoded, chosen by the client for this one connection
Sec-WebSocket-Version: 13The protocol version. 13 is the version everyone uses
OriginWhich web page opened the connection. Browsers add it automatically (see the security section below)
Sec-WebSocket-AcceptThe server's proof that it understood the request (explained next)

How Sec-WebSocket-Accept is built

The server takes the client's key, appends a fixed text value defined in the standard (258EAFA5-E914-47DA-95CA-C5AB0DC85B11), computes the SHA-1 hash of the result and encodes it as base64. You can reproduce it in Node.js:

const crypto = require('crypto');

const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const key = 'dGhlIHNhbXBsZSBub25jZQ==';

const accept = crypto.createHash('sha1').update(key + GUID).digest('base64');
console.log(accept);   // s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Note: This is not a security feature. Its only job is to prove that the server really understood a WebSocket request, so that an ordinary web server or cache cannot accidentally "answer" it. Anyone can compute it. Security comes from wss:// and from your own checks on the server.

Frames: What a Message Looks Like

After the handshake, data travels in small packets called frames. A message is one frame, or several frames when it is split up (the first frame has the type, the later ones are continuation frames, and the last one has FIN = 1). Every frame starts with a short header:

Layout of a WebSocket frame A frame is drawn as rows 32 bits wide. The first row holds FIN (1 bit), RSV1 to RSV3 (1 bit each), opcode (4 bits), MASK (1 bit) and payload length (7 bits). An optional row holds an extended payload length of 16 or 64 bits, used when the 7-bit value is 126 or 127. Another optional row holds a 32-bit masking key, present only when MASK is 1. The payload data follows. A strip below explains the length rule. each row is 32 bits wide (the first row is not drawn to scale) FIN 1 bit RSV1 1 bit RSV2 1 bit RSV3 1 bit opcode 4 bits MASK 1 bit Payload length 7 bits 0-125, or 126 / 127 (see below) Extended payload length (optional) 16 bits if the 7-bit value is 126, 64 bits if it is 127, absent if it is 0-125 Masking key, 32 bits (only when MASK = 1) client-to-server frames always carry one; server-to-client frames do not Payload data text or binary bytes; in a masked frame each byte is XORed with the masking key How to read the length 7-bit value 0 to 125 that is the payload length 7-bit value 126 next 16 bits are the length 7-bit value 127 next 64 bits are the length
The smallest frame header is only 2 bytes (plus 4 bytes of masking key for frames sent by a client). Compare that with hundreds of bytes of HTTP headers per request.

The opcode says what kind of frame this is:

OpcodeNameMeaning
0x0ContinuationAnother piece of a message that started in an earlier frame
0x1TextThe payload is UTF-8 text
0x2BinaryThe payload is raw bytes (images, audio, your own format)
0x8Close"I am closing." Carries a status code and an optional reason
0x9Ping"Are you still there?" Either side may send it
0xAPongThe answer to a ping, repeating the ping's data
  • Masking. Every frame a client sends must be masked: the payload is XORed with a random 4-byte key placed in the frame. Frames from the server must not be masked. Masking is not encryption (the key travels with the frame). It exists so that a badly behaved proxy cannot be tricked by data that looks like HTTP. Browsers and libraries do it for you.
  • Control frames (close, ping, pong) are small: their payload is at most 125 bytes and they are never split into several frames.
  • RSV1 to RSV3 are reserved for extensions. The common permessage-deflate extension, which compresses messages, uses RSV1.

The Life of a Connection

In the browser the connection is always in one of four states, available as socket.readyState. While it is open, the two sides can check that the line is still alive with ping and pong frames, and either side ends the session with a close frame.

Lifecycle of a WebSocket connection The connection moves through four states: CONNECTING (readyState 0), OPEN (1), CLOSING (2) and CLOSED (3). Below, a sequence shows a ping frame answered by a pong frame as a heartbeat, then one side sending a Close frame with status code 1000 and the other side answering with a Close frame, after which the TCP connection is closed. The four states CONNECTING readyState 0 OPEN readyState 1 CLOSING readyState 2 CLOSED readyState 3 handshake in progress (waiting for the 101) onopen fires; messages flow in both directions a Close frame has been sent, waiting for the reply connection finished; onclose fires Heartbeat and closing, frame by frame Client Server Heartbeat Ping (0x9), sent by either side Pong (0xA), repeats the ping data Closing Close (0x8) with status code 1000 Close (0x8) sent back in reply then the TCP connection is closed
Ping and pong keep an idle connection alive and prove the other side is still there. A clean close is a short two-frame conversation.

A close frame begins with a 2-byte status code. The ones you will see most often:

CodeMeaning
1000Normal closure: the work is done
1001Going away, for example the server is shutting down or the user left the page
1006Abnormal closure. Never sent on the wire: the browser reports it when the connection was lost without a close frame
1008Policy violation, for example a failed Origin or authentication check
1009Message too big
1011The server hit an unexpected error

Browser Example

Browsers have WebSocket built in. There is nothing to install:

const socket = new WebSocket('wss://example.com/chat');

socket.onopen = (event) => {
  console.log('Connected');
  socket.send(JSON.stringify({ type: 'hello', name: 'Asha' }));
};

socket.onmessage = (event) => {
  const msg = JSON.parse(event.data);   // event.data is a string for text frames
  console.log('Received:', msg);
};

socket.onerror = (event) => {
  console.log('Connection error');       // the event carries no details on purpose
};

socket.onclose = (event) => {
  console.log('Closed with code', event.code, event.reason);
};

Call socket.send(...) only after onopen has fired (or when socket.readyState === WebSocket.OPEN), and socket.close() when you are done. The browser answers the server's pings automatically; your JavaScript cannot send pings itself, so apps that need a heartbeat usually send a small message of their own every so often.

Node.js Server Example

One of the most popular libraries for Node.js is ws. This small server echoes every message it receives to all connected clients, which is already a working chat room:

// npm install ws
const { WebSocketServer, WebSocket } = require('ws');

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('A client connected');

  ws.on('error', console.error);

  ws.on('message', (data, isBinary) => {
    // send the message to everyone who is still connected
    for (const client of wss.clients) {
      if (client.readyState === WebSocket.OPEN) {
        client.send(data, { binary: isBinary });
      }
    }
  });

  ws.on('close', () => console.log('A client left'));
});

console.log('Listening on ws://localhost:8080');
Note: Run this on your own computer (node server.js), not on this website. Then connect to ws://localhost:8080 with the browser code above from a page you open locally. Plain ws:// is fine for local testing; use wss:// in production.

Where WebSocket Is Used

  • Chat and messaging. A message is delivered the moment someone sends it.
  • Live dashboards and prices. Stock tickers, sports scores, monitoring graphs and order books update without reloading.
  • Notifications. The server tells an open page about a new comment, a finished job or a payment.
  • Multiplayer games. Many small, frequent messages in both directions with low delay.
  • Collaborative editing. Every keystroke or cursor move is shared with the other people in the document.

If the data only flows one way and rarely, plain HTTP or SSE may be simpler. For tiny devices that publish sensor data, see MQTT, which solves a related problem with a lighter model.

WebSocket vs Polling vs Server-Sent Events

WebSocketPolling / long pollingServer-Sent Events
DirectionBoth ways, at any timeClient asks, server answersServer to client only
ConnectionOne long-lived connectionA new request for every check (long polling holds each one open for a while)One long-lived HTTP response
OverheadA few bytes of framing per messageFull HTTP headers and cookies on every requestSmall, plain text events
Reconnect after a dropYou write it yourselfNatural: the next poll simply tries againBuilt into the browser's EventSource
DataText or binaryAnything HTTP can carryText only
Browser supportAll modern browsersEverywhereAll modern browsers
Proxies and firewallsUsually fine over wss://; some old or strict proxies interfereWorks wherever HTTP worksWorks over HTTP, but some proxies buffer the stream
Best forChat, games, collaborationSimple, infrequent updatesFeeds, notifications, progress

Production Issues and Common Mistakes

  • Idle connections get dropped. Proxies and load balancers often close a connection after a period of silence (frequently around a minute, but it varies). Send a ping from the server, or a small message from the client, every 20 to 30 seconds, and close connections that stop answering.
  • No reconnect logic. Networks drop, phones change from Wi-Fi to mobile data, servers restart. Reconnect automatically with exponential backoff (wait 1 s, then 2 s, 4 s and so on up to a limit, ideally with some randomness) so that thousands of clients do not hammer the server at the same moment. After reconnecting, subscribe again and ask for the messages you missed. The server has forgotten you.
  • Using ws:// in production. Always use wss://. It protects the data and also passes through more networks.
  • Trusting the browser's origin policy. Browsers do not apply the same-origin rule to WebSocket. Any web page can try to open a connection to your server, and the browser will include your user's cookies. The server must check the Origin header itself.
  • Skipping authentication. Authenticate in the handshake with a session cookie, or send a short-lived token as the first message. Browser JavaScript cannot add custom headers to a WebSocket handshake. Avoid putting tokens in the URL query string: URLs are written to logs and history.
  • Unlimited message size. One huge message can use up the server's memory. Set a maximum (the ws library has a maxPayload option), validate every message as untrusted input, and limit how many messages a client may send per second.
  • Forgetting that connections are stateful. Each open connection lives on one server. To scale out, use sticky sessions so a client keeps reaching the same server, and a shared message backend (for example a pub/sub system such as Redis) so that a message received by one server reaches clients connected to the others.

A simple Origin check with ws looks like this:

wss.on('connection', (ws, req) => {
  if (req.headers.origin !== 'https://example.com') {
    ws.close(1008, 'Origin not allowed');   // 1008 = policy violation
    return;
  }
  // ... authenticate, then handle messages
});

And a minimal reconnect loop in the browser:

let delay = 1000;                      // start with one second

function connect() {
  const socket = new WebSocket('wss://example.com/chat');

  socket.onopen = () => {
    delay = 1000;                      // connected again: reset the wait
    // subscribe to your channels again here
  };

  socket.onclose = () => {
    setTimeout(connect, delay);
    delay = Math.min(delay * 2, 30000);   // double the wait, at most 30 s
  };
}

connect();
Recap: Plain HTTP cannot push data, and polling wastes requests. WebSocket starts as an HTTP GET with Upgrade: websocket, the server answers 101 Switching Protocols with a Sec-WebSocket-Accept value, and the same TCP connection then carries small frames in both directions. Frames have an opcode (text, binary, close, ping, pong), and client frames are always masked. Use wss://, send heartbeats, reconnect with backoff, check the Origin and authenticate on the server, limit message sizes, and plan for sticky sessions or a pub/sub backend when you scale.
Next steps: Continue to MQTT, a lightweight publish and subscribe protocol for devices and sensors. To use WebSocket in an app, see Android WebSockets.