WebSocket Fundamentals: A Practical Intro for Developers Building Real-Time Apps
A clear beginner's guide to WebSocket fundamentals—how the upgrade handshake works, when to use it, and how to send your first message in plain JavaScript.
Real-time notifications, live prices, and chat messages may need updates initiated by the server. WebSockets provide a persistent channel for two-way communication; compare that requirement with simpler one-way alternatives before choosing the protocol.
What a WebSocket Is
A WebSocket is a persistent, full-duplex communication channel between a client and a server. "Full-duplex" means both sides can send messages independently and simultaneously, like a phone call rather than a walkie-talkie.
It starts as a standard HTTP request. The client sends an Upgrade: websocket header. If the server accepts, both sides switch to the WebSocket protocol over the same TCP connection. From that point on, the connection stays open until either side closes it, and either side can send a message at any time.
This is fundamentally different from standard HTTP, where:
- The client must always initiate
- The server sends exactly one response per request
- The connection closes after the response
With WebSockets, the server can push data whenever it wants, without any polling from the client.
The WebSocket Handshake
The upgrade happens through a standard HTTP GET request with specific headers:
GET /realtime HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
The server responds with a 101 Switching Protocols status and a computed Sec-WebSocket-Accept header. After this exchange, the TCP connection is handed off to the WebSocket protocol. Standard HTTP proxies and firewalls generally handle this upgrade correctly on modern infrastructure, though some corporate environments require explicit allowlisting.
Sending Your First WebSocket Message in the Browser
The browser's built-in WebSocket API is straightforward:
const socket = new WebSocket('wss://api.example.com/realtime');
socket.addEventListener('open', () => {
console.log('Connected');
socket.send(JSON.stringify({ type: 'ping' }));
});
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
});
socket.addEventListener('close', (event) => {
console.log('Connection closed:', event.code, event.reason);
});
socket.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});Use wss:// (WebSocket Secure, over TLS) in production, never plain ws://. The error event rarely carries useful detail on its own — the subsequent close event with its code and reason is more informative.
A Minimal Node.js WebSocket Server
Using the ws library, the server side is equally direct:
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (socket, request) => {
console.log('Client connected');
socket.on('message', (raw) => {
const msg = JSON.parse(raw);
console.log('Received:', msg);
socket.send(JSON.stringify({ type: 'pong' }));
});
socket.on('close', () => {
console.log('Client disconnected');
});
});The ws package provides WebSocket support for Node.js. Socket.IO is a separate protocol and library with features such as rooms and reconnection; it can use WebSocket as a transport, but its clients are not interchangeable with plain WebSocket clients.
WebSocket Message Formats
WebSocket frames can carry text or binary data. Most application-level protocols use text frames with JSON payloads because JSON is easy to debug and universally supported. A common pattern is an envelope with a type field and a payload:
{
"type": "cursor_moved",
"payload": {
"userId": "u_123",
"x": 412,
"y": 207
}
}For high-throughput systems where payload size matters — game state, financial ticks — binary formats like MessagePack or Protocol Buffers over binary frames can cut bandwidth meaningfully. Start with JSON unless you have a measured reason to optimize.
Close Codes and What They Mean
When a WebSocket closes, it carries a numeric code. The most important ones:
| Code | Meaning |
|---|---|
| 1000 | Normal closure |
| 1001 | Going away (server shutting down) |
| 1006 | Abnormal closure (no close frame received) |
| 1011 | Internal server error |
| 1013 | Server at capacity |
| 4001 | Application-level: unauthorized (custom) |
Codes in the 4000-4999 range are reserved for application-defined use. Use them to communicate application-specific close reasons to the client.
What WebSockets Do Not Handle Automatically
Understanding the gaps is as important as the basics:
- Reconnection — the browser does not reconnect automatically on close; you must implement this yourself
- Heartbeats — dead connections are not surfaced immediately; you need ping/pong to detect them
- Message ordering guarantees — WebSockets are ordered within a connection, but if the client reconnects, it has no built-in mechanism to catch up on missed messages
- Authentication — the WebSocket handshake can carry cookies and headers, but most production systems implement an explicit auth frame after connection
These are the features you will need to build, or use a library that provides them.
When to Use WebSockets (and When Not To)
Use WebSockets for:
- Chat and messaging
- Live document collaboration
- Real-time dashboards where users also send commands
- Multiplayer features
Consider SSE or long polling instead for:
- Notification feeds (server-to-client only)
- Live data displays with no client-to-server interaction
- Deployments behind infrastructure that does not reliably support WebSocket upgrades
Test disconnection, reconnection, authentication expiry, and slow consumers alongside normal message delivery.