WebSocket is a communication protocol, standardized as RFC 6455, that turns a single HTTP request into a persistent, full-duplex connection over which a browser and a server can send messages to each other at any time. Among the Web APIs, it is exposed to scripts through the WebSocket interface, which lets a page open a connection, send text or binary data and react to incoming messages through events.
The protocol exists because classic HTTP is request-response: the client asks, the server answers, and the server cannot speak first. WebSocket removes that constraint after a brief opening handshake, which makes it the standard choice for chat, live dashboards, multiplayer games, collaborative editing and trading screens where low latency in both directions matters more than cacheability.
How the WebSocket handshake and framing work
A WebSocket connection starts as an ordinary HTTP/1.1 GET request carrying the headers Upgrade: websocket, Connection: Upgrade, a random Sec-WebSocket-Key and Sec-WebSocket-Version: 13. A server that accepts replies with status 101 Switching Protocols and a Sec-WebSocket-Accept value derived from the key, which proves it understood the request. From that moment the same TCP connection carries WebSocket frames instead of HTTP messages.
Data travels in frames with a compact header of 2 to 14 bytes, far smaller than the headers of a typical HTTP request. Each frame carries an opcode that identifies its type, a payload length and, for frames sent by the client, a 32-bit masking key. Masking does not provide confidentiality; it exists to stop malicious scripts from poisoning intermediary caches with crafted byte sequences. Large messages can be split across several frames and reassembled by the receiver.
The following table lists the frame types defined by the protocol and what each one does.
| Opcode | Frame type | Purpose |
|---|---|---|
| 0x0 | Continuation | Carries the next fragment of a split message |
| 0x1 | Text | UTF-8 encoded message, typically JSON |
| 0x2 | Binary | Raw bytes, such as Protocol Buffers or media |
| 0x8 | Close | Starts the closing handshake with a status code |
| 0x9 | Ping | Checks that the peer is still alive |
| 0xA | Pong | Answers a ping or acts as a one-way heartbeat |
Connections use the ws:// scheme on port 80 or the encrypted `wss://` scheme on port 443, where the frames run inside TLS exactly as HTTPS traffic does. In practice `wss://` is the only sensible choice on the public internet, because proxies and middleboxes often break or block unencrypted upgrades.
The browser WebSocket API
In the browser, a script creates a connection with new WebSocket(url, protocols) and works with it through four events and two methods. The optional protocols argument negotiates an application subprotocol through the Sec-WebSocket-Protocol header, so both sides agree on the message format, for example STOMP or a custom JSON schema.
The table below summarizes the members a frontend developer uses most often.
| Member | Kind | What it does |
|---|---|---|
open | Event | Fires when the handshake succeeds |
message | Event | Delivers each received message in event.data |
error | Event | Signals a failure, always followed by close |
close | Event | Reports the close code, reason and wasClean flag |
send() | Method | Queues text, a Blob or an ArrayBuffer for sending |
close() | Method | Starts a clean shutdown with an optional code |
readyState | Property | Reports CONNECTING, OPEN, CLOSING or CLOSED |
bufferedAmount | Property | Counts bytes queued but not yet transmitted |
The classic API has no built-in backpressure: send() never blocks, so a fast producer can fill memory while bufferedAmount grows. Careful code checks that property before sending more. The newer, promise-based WebSocketStream proposal addresses this with streams, but its browser support remains limited.
WebSocket compared with other real-time techniques
WebSocket is one of several ways to push data to a page, and it is not always the best one. Its neighbors in the Web APIs topic, especially Server-Sent Events and WebRTC, solve related problems with different trade-offs.
| Technique | Direction | Transport | Best fit |
|---|---|---|---|
| WebSocket | Both ways | Upgraded TCP connection | Chat, games, collaborative editing |
| Server-Sent Events | Server to client | Plain HTTP stream | Notifications, feeds, live scores |
| Long polling | Server to client, emulated | Repeated HTTP requests | Legacy fallback behind strict proxies |
| WebRTC data channel | Peer to peer | SCTP over DTLS and UDP | Low-latency peer traffic, media |
| WebTransport | Both ways, multiple streams | HTTP/3 over QUIC | Unordered or unreliable delivery needs |
Server-Sent Events are simpler when updates flow only from the server, because they reuse standard HTTP, reconnect automatically and multiplex well over HTTP/2. WebSocket wins when the client also sends frequent messages and when binary payloads are involved. A connection runs over TCP, so it inherits head-of-line blocking: one lost packet delays every message behind it, which is why fast-paced games sometimes prefer WebRTC or WebTransport.
Running WebSocket in production
A WebSocket server holds one long-lived connection per client, so capacity planning is about concurrent connections and memory rather than requests per second. A well-tuned event-driven server can keep tens of thousands of idle connections per instance, but the real limit depends on message rates, per-connection state and file-descriptor limits.
- Load balancers and proxies must support the upgrade and use idle timeouts longer than the heartbeat interval, often 30 to 60 seconds.
- Horizontal scaling needs a shared channel, such as a publish-subscribe broker, so a message reaches clients connected to other instances.
- Reconnection with exponential backoff and jitter prevents a thundering herd when a server restarts and thousands of clients return at once.
- Message resumption with sequence numbers lets a client request what it missed during a short disconnect.
- Compression through the
permessage-deflateextension saves bandwidth but costs CPU and memory per connection.
Security risks and common mistakes
Browsers do not apply the same-origin policy to WebSocket connections the way they do to fetch, and they send cookies with the handshake. A server that does not validate the `Origin` header is therefore exposed to cross-site WebSocket hijacking, in which a malicious page opens an authenticated connection on behalf of the victim.
The table pairs frequent problems with their usual cause and fix.
| Symptom | Likely cause | Fix |
|---|---|---|
| Connections drop after about 60 seconds | Proxy idle timeout | Send pings and raise proxy timeouts |
| Messages lost after reconnect | No resumption logic | Add sequence numbers and replay |
| Server memory keeps growing | Slow clients, no backpressure | Cap buffers and disconnect laggards |
| Foreign sites act as the user | Missing Origin check | Allowlist origins and use tokens |
| Handshake fails on some networks | Unencrypted ws:// blocked | Use wss:// on port 443 |
Authentication deserves particular care. The browser API cannot set custom headers on the handshake, so teams pass a short-lived token in the first message or in a cookie rather than placing long-lived credentials in the URL, where logs may record them. Every incoming message must also be validated like any other untrusted input.
The short version
A normal web page talks to a server like someone sending letters: it asks a question, waits for the reply, then asks again. The server can never write first.
WebSocket turns that exchange into a phone call that stays open. After a quick greeting, both sides can talk whenever they have something to say, without dialing again each time. That is why chat messages, live scores and shared documents can update instantly without the page constantly asking whether anything has changed.
Where it makes a difference
A logistics company runs a dispatch dashboard showing the positions of about 2,000 delivery vans. The first version refreshed the map by polling a REST endpoint every five seconds, which produced roughly 400 requests per second at peak, mostly returning unchanged data, while positions still appeared several seconds late.
The team replaced polling with a wss:// connection per dispatcher. The server now pushes only changed positions as small binary frames, sends a ping every 25 seconds to keep proxies from closing idle connections, and tags each update with a sequence number so a browser that reconnects can request what it missed.
The result was a sharp drop in request volume and updates that reach the screen in well under a second. Dispatchers can also send reassignment commands over the same connection, so a change made by one dispatcher appears on every other screen almost immediately.