
WebSocket: Escape from Refresh Hell
HTTP is Walkie-Talkie (Over). WebSocket is Phone (Hello). The secret tech behind Chat and Stock Charts.

HTTP is Walkie-Talkie (Over). WebSocket is Phone (Hello). The secret tech behind Chat and Stock Charts.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

Imagine implementing chat in the old web. It was truly frustrating. HTTP uses a request-response model, so the server only speaks when the client asks. To check if a friend sent a message? You had to manually hit Refresh (F5). The server stayed passive, keeping its mouth shut until the client asked.
To automate this a bit, I initially tried Polling with setInterval, sending "Any messages?" to the server every second. But this was terribly inefficient. Even when there were no messages, requests kept flying, wasting server resources.
// My dark past: Polling every second
setInterval(() => {
fetch('/api/messages')
.then(res => res.json())
.then(data => console.log(data));
}, 1000);
"New msg?" (No) "New msg?" (No) "New msg?" (Yes, here)
With 100 concurrent users? The server answers 100 meaningless questions every second. The king of traffic waste.
So an improved version called Long Polling emerged. Instead of responding immediately, the server holds the connection until new data arrives. When data appears, it responds, and the client immediately sends another request.
// Long Polling pattern
function longPoll() {
fetch('/api/messages/long-poll')
.then(res => res.json())
.then(data => {
console.log(data);
longPoll(); // Re-request immediately after response
});
}
longPoll();
This is better than regular polling, but still repeats the HTTP request-response cycle with significant overhead from constantly opening and closing connections. It's "pseudo real-time" at best.
What I understood as the core of WebSocket: "Once you establish a connection, keep it open until it closes."
If HTTP is a temp worker who hangs up after the job is done, WebSocket is a permanent employee who stays on the line after connecting. This analogy clicked for me.
HTTP (Walkie-Talkie):
WebSocket (Phone):
After accepting this difference, I understood why "real-time" is implemented with WebSocket.
WebSocket also starts with HTTP. This was interesting. The client sends an HTTP request saying "I want to upgrade to WebSocket," and the server says "Okay" and switches protocols. This is the Handshake process.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Server response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
101 Switching Protocols is the key status code. It means "From now on, we're speaking WebSocket, not HTTP." From this moment, the connection switches to ws:// (or encrypted wss://) protocol, and the pipe stays open until disconnection.
After understanding this handshake mechanism, I figured out what "WebSocket runs on top of HTTP" means. It starts with HTTP, then switches protocols midway.
Using WebSocket in the browser is really simple.
// Create WebSocket connection
const socket = new WebSocket('ws://localhost:8080');
// When connection opens
socket.addEventListener('open', (event) => {
console.log('WebSocket connected');
socket.send('Hello, Server!'); // Send message to server
});
// When receiving message from server
socket.addEventListener('message', (event) => {
console.log('Message from server:', event.data);
});
// On error
socket.addEventListener('error', (event) => {
console.error('WebSocket error:', event);
});
// When connection closes
socket.addEventListener('close', (event) => {
console.log('WebSocket closed', event.code, event.reason);
});
When I first saw this code, I thought "That's it?" Unlike HTTP fetch with repeated request-response, you just socket.send() and receive via message event. Both server and client can send messages first anytime.
For Node.js WebSocket servers, the ws library is popular.
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('New client connected');
// When receiving message from client
ws.on('message', (message) => {
console.log('Received:', message);
// Broadcast to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(`Broadcast: ${message}`);
}
});
});
// Connection close
ws.on('close', () => {
console.log('Client disconnected');
});
// Send welcome message
ws.send('Connected to server!');
});
console.log('WebSocket server running on port 8080');
After running this code myself, I understood what "connection-based" means. When the connection event fires, you receive that connection (ws) as an object and keep holding it to send messages later. In HTTP, the connection closes after sending a response, but in WebSocket, you can keep communicating as long as the connection object is alive.
Like HTTP and HTTPS, WebSocket has two protocols based on encryption:
In production, you obviously need wss://. There's risk of data interception otherwise. Especially when exchanging auth tokens or sensitive info via WebSocket, it's essential.
WebSocket connections go through 4 main events:
What I understood: While the connection is open, you can send() messages anytime. No need to create new requests like HTTP. When connection drops, you need to implement reconnection logic yourself.
WebSocket connections are TCP-based, so if one side dies suddenly, the other might not know. For example, if a client closes their laptop, the server might keep holding that dead connection. To prevent this, we use Heartbeat or Ping-Pong mechanisms.
The server periodically sends Ping frames, and if the client doesn't respond with Pong, it judges "this connection is dead" and cleans it up.
// Server-side Ping-Pong example
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate(); // Terminate if no response
}
ws.isAlive = false;
ws.ping(); // Send Ping
});
}, 30000); // Every 30 seconds
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true; // Mark alive when Pong received
});
});
After accepting this pattern, my question "Why do WebSocket connections weirdly disconnect and reconnect?" was solved. Without heartbeat, zombie connections can pile up and waste server resources.
When first learning WebSocket, I mistakenly thought "Socket.io is WebSocket." But precisely, Socket.io is a library that uses WebSocket, but has fallback functionality that automatically switches to long polling in environments that don't support WebSocket.
Socket.io advantages:
Native WebSocket advantages:
My criteria: Native WebSocket for simple real-time features, Socket.io for complex chat/notification systems.
WebSocket isn't the only real-time technology. There's SSE (Server-Sent Events), which supports unidirectional server → client push only. The client establishes connection via HTTP request, and the server keeps streaming events.
// Client (browser)
const eventSource = new EventSource('/events');
eventSource.onmessage = (event) => {
console.log('New event:', event.data);
};
// Server (Node.js + Express)
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const interval = setInterval(() => {
res.write(`data: ${new Date().toISOString()}\n\n`);
}, 1000);
req.on('close', () => {
clearInterval(interval);
});
});
SSE runs on top of HTTP and supports automatic reconnection. However, client → server direction requires separate HTTP requests. So it's suitable for cases like "stock prices" where "server only needs to push". For bidirectional communication, use WebSocket.
One of WebSocket's biggest challenges is Scale-Out. When you have multiple servers, client A might connect to server 1, and client B to server 2. In this state, if A wants to message B? Server 1 needs to forward it to server 2.
Load balancer configured to route the same client always to the same server. Based on client IP or cookie. But this half-kills the scaling advantage. If server 1 dies, all clients attached to it get disconnected.
A better approach is Redis Pub/Sub pattern. Each server subscribes to Redis channels and publishes messages. When client A sends a message to server 1, server 1 publishes to Redis, and server 2 (where client B is connected) subscribes and receives it.
// Server-side (Redis Pub/Sub example)
const redis = require('redis');
const publisher = redis.createClient();
const subscriber = redis.createClient();
subscriber.subscribe('chat');
subscriber.on('message', (channel, message) => {
// Broadcast to all clients connected to this server
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
wss.on('connection', (ws) => {
ws.on('message', (msg) => {
// Publish to Redis (other servers will receive)
publisher.publish('chat', msg);
});
});
After understanding this pattern, my question "Why do chat servers use Redis together?" was solved. WebSocket is stateful and attached to servers, so cross-server message synchronization is essential.
This is the most important point: Most of the time, WebSocket is unnecessary. REST API is sufficient.
Don't use WebSocket for:
Use WebSocket for:
Initially, I tried applying "real-time = WebSocket" unconditionally, only to waste server resources. First consider if server push is really needed. For example, news feeds that "update once per minute is fine" can use simple polling or SSE.
WebSocket is "the technology that doesn't hang up the phone". HTTP hangs up after business is done, but WebSocket keeps the connection alive. It upgrades protocol from HTTP to WebSocket via handshake, then enables bidirectional communication.
In browsers, simply use new WebSocket(), and servers implement with ws library or Socket.io. ws:// is plaintext, wss:// is encrypted. Use heartbeat to clean zombie connections, and Redis Pub/Sub to sync messages across multiple servers.
But you don't need WebSocket for everything. First consider if server push is truly necessary, or if SSE is sufficient. Using WebSocket for problems solvable with REST API only increases complexity.
Now when implementing chat, I don't poll every second with setInterval. I establish one WebSocket connection and receive messages only when they arrive. This is what I understood as the secret of "real-time".