GICT knowledge base
WebSockets Explained: How Real-Time Communication Actually Works
Published
HTTP is a one-shot protocol. You send a request, you get a response, the connection closes. That model works fine for most things. It falls apart the moment you need a server to push data to a client without the client asking first. Notifications, live dashboards, chat applications, collaborative documents all of these need a channel the server can write to freely. WebSockets are the answer. They take an HTTP connection and turn it into a persistent, bidirectional pipe that either side can write to at any time. This article explains how that works and how to test it yourself on a VPS.
Why HTTP Is Not Enough
HTTP was designed for document retrieval, not ongoing conversations. The client always initiates. The server always responds. If the server has new information five minutes after the last request, it has no way to reach the client.
The workarounds existed before WebSockets. Polling sends requests on a timer wasteful. Long-polling holds a request open until the server has something to return more efficient but complex and still limited. Both are hacks that pretend HTTP is something it is not. WebSockets solve this properly by keeping the connection open after the initial handshake.
The Upgrade: From HTTP to WebSocket
A WebSocket connection starts as a regular HTTP request. The client sends a handshake asking to upgrade the connection:
GET /ws HTTP/1.1
Host: your-server.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
If the server supports WebSockets, it responds with a 101 status code:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
The 101 means “I am switching protocols.” After this exchange, both sides can send data frames over the same TCP connection. No more HTTP rules just raw frames.
The Sec-WebSocket-Key is a security measure. The server uses it to prove it actually handled the handshake, preventing cross-site WebSocket hijacking. It is a simple challenge-response mechanism built into the protocol.
What WebSocket Frames Look Like
After the handshake, data moves in frames. A frame has a small header of 2 to 14 bytes plus the payload. The first byte tells you if this is a text frame, binary frame, or a control message like a close signal.
Text frames carry UTF-8 data. Binary frames carry anything else, like images or serialized objects. Close frames are control messages that gracefully shut the connection down. Ping and pong frames serve as heartbeat signals to keep the connection alive through proxies and load balancers that close idle connections.
You rarely read frames manually. Client libraries and server frameworks handle the framing. But the wire format being lightweight matters: WebSocket overhead per message is just a few bytes, compared to the kilobytes of headers a full HTTP request carries.
What You Can Build with WebSockets
WebSockets are the backbone of anything that needs real-time updates. The server maintains a persistent connection to each client, which means it can push data at any time without waiting for the client to ask.
- Chat applications where messages appear instantly for all participants
- Live dashboards that refresh without the user doing anything
- Collaborative documents where multiple people see changes as they happen
- Notifications that push to the browser without a page refresh
- Online games where latency matters
The common thread across all of these: the server needs to reach the client, not just respond to the client.
A Simple WebSocket Server on Your VPS
Node.js makes WebSockets straightforward. Install the ws library:
npm install ws
Here is a server that accepts connections and echoes any message back to the client:
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
server.on('connection', ws => {
console.log('Client connected');
ws.on('message', message => {
console.log('Received:', message.toString());
ws.send('Echo: ' + message);
});
ws.on('close', () => console.log('Client disconnected'));
});
console.log('WebSocket server running on port 8080');
Save this as server.js and run it with node server.js. This is the bare minimum. A production server handles thousands of concurrent connections, routes messages between clients, and manages authentication.
Testing WebSocket Connections
You can test a WebSocket connection directly from the terminal using wscat:
npm install -g wscat
wscat -c ws://your-server.com:8080
Type messages and press Enter. If the server echoes, you see the response immediately.
You can also test from a browser. Open DevTools, go to the Console, and run:
const ws = new WebSocket('ws://your-server.com:8080');
ws.onmessage = event => console.log('Received:', event.data);
ws.send('Hello');
The browser DevTools Network tab also shows WebSocket connections if you filter by WS.
WSS: WebSockets Over SSL
Just like HTTP becomes HTTPS, WebSockets become WSS when tunneled through TLS. The handshake is identical, but the connection is encrypted. Browsers prefer WSS in environments where mixed HTTPS and non-SSL content causes warnings.
Configure WSS by terminating the certificate at Nginx and proxying WebSocket connections:
location /ws {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
Nginx handles the SSL. Your WebSocket server receives plain WebSocket frames on the inside. The two lines starting with proxy_set_header are the Upgrade headers they tell Nginx to forward the WebSocket handshake and keep the connection alive.
Common Problems and How to Debug Them
WebSocket issues are harder to debug than HTTP because there is no request-response structure. Here are the most common ones.
Connection closes immediately. Check the server logs first. Most likely causes: handshake rejected because of a wrong Sec-WebSocket-Key, origin not allowed by the server, or the server process crashed during initialization.
Messages not arriving. Verify the server is routing to the right client. If you have multiple clients connected, make sure you are sending to the correct WebSocket object and not to a different one.
Works in the browser but not in wscat. The browser adds an Origin header automatically. Some servers validate this and reject connections from unknown origins. Pass the origin explicitly with wscat:
wscat -c --origin https://your-site.com ws://your-server.com:8080
Nginx proxy drops connections after 60 seconds. This is a known Nginx behavior it closes idle connections after 60 seconds by default. Fix it by adding to your location block:
proxy_read_timeout 86400;
Nginx drops WebSocket connections immediately. This almost always means the Upgrade and Connection headers are missing or misspelled. They must be set exactly as shown above.
Summary
WebSockets solve the fundamental limitation of HTTP: the server cannot reach the client without being asked. The connection starts as an HTTP handshake with a 101 status code, then switches to a persistent bidirectional pipe. Data moves in lightweight frames that cost just a few bytes each.
You can build chat, live dashboards, collaborative tools, and real-time notifications with it. On a VPS, a simple Node.js WebSocket server takes minutes to set up. For production use, always use WSS, put it behind Nginx with the Upgrade headers configured correctly, and add proxy_read_timeout to prevent idle connection drops.
One thing worth knowing: WebSocket connections are long-lived by design. A server handling thousands of concurrent WebSocket connections needs to manage memory carefully. Node.js handles this well with event-driven architecture, but it is a different mental model than request-response HTTP where each request is short-lived.
Related guides
Cookies vs LocalStorage vs SessionStorage: What Each One Exactly Means
A simplest breakdown of the three ways browsers store data: cookies, localStorage, and sessionStorage.
web developmentHow to Run Multiple PHP Versions on Nginx with PHP-FPM
Learn how to run multiple PHP versions on Nginx with PHP-FPM through a step-by-step guide, enabling seamless support for different applications.
web developmentHow to Fix 413 Request Entity Too Large Error in Nginx (5 Working Solutions)
Learn five effective solutions to fix the 413 Request Entity Too Large error in Nginx to ensure seamless file uploads.
web developmentUnderstanding CORS and Why Browsers Block Cross-Origin Requests
Understand CORS, its system of cross-origin requests, how to configure it in web servers like Nginx and Apache, and common errors and solutions.
