Every time a chat message appears without you hitting refresh, a stock price ticks up in real time, or a game renders another player's move instantly, that's WebSockets at work. HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. alone can't do this cleanly. WebSockets were invented precisely for this kind of live, two-way communication.
The problem with HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. for real-time
HTTP follows a strict rule: the client always speaks first. You send a request, the server replies, and the connection closes. This works perfectly for loading a webpage or fetching a list of products. But what if the server has new data and you haven't asked for it yet?
The simplest workaround is called pollingWhat is polling?Repeatedly asking a server at regular intervals if anything has changed, which works but wastes resources when nothing is new.: your client repeatedly asks "anything new?" on a timer:
Client: Any news? → Server: No.
Client: Any news? → Server: No.
Client: Any news? → Server: No.
Client: Any news? → Server: Yes! Here's a new message.This gets the job done, but it's brutally inefficient. Every "any news?" is a full HTTP request complete with headers, cookies, and authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. tokens, even when the answer is always "no". Scale that to thousands of users checking every two seconds and you have a server spending most of its time saying nothing.
How WebSockets solve this
A WebSocket connection starts as a regular HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. request, called the handshakeWhat is handshake?The initial exchange between a client and server that establishes a connection and agrees on communication rules before data starts flowing., and then upgrades to a persistent, bidirectional channelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue).. Once that channel is open, both client and server can send messages whenever they want. No asking permission, no new connection, no repeated headers.
Think of the difference between mailing letters and having a phone call. HTTP is letters: send one, wait for a reply, send another. WebSockets are a phone call: once you're connected, either side can speak at any moment.
Client → Server: "I'd like to upgrade to WebSocket."
Server → Client: "Done. Connection established."
[connection stays open indefinitely]
Server → Client: "New message from Alice!"
Client → Server: "Here's my reply to Alice."
Server → Client: "Bob just joined the room."The WebSocket handshake
The upgrade starts as a regular HTTP request with a special Upgrade header:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13The server responds with 101 Switching Protocols. From that moment, the connection is no longer HTTP, it's WebSocket. This handshake happens exactly once per connection.
HTTP vs WebSocket at a glance
| Feature | HTTP | WebSocket |
|---|---|---|
| Who initiates messages? | Client only | Either side |
| New connection per message? | Yes | No, one persistent connection |
| Headers on every message? | Yes | No |
| Server can push data? | No | Yes |
| Best for | APIs, page loads | Chat, live dashboards, multiplayer games |
The browser WebSocket APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses.
No library required, every modern browser has WebSocket support built in. The API is deliberately minimal: one constructor and four event handlers.
// Open a connection
const socket = new WebSocket('wss://my-server.com/chat');
// Fires when the connection is ready to use
socket.onopen = () => {
console.log('Connected!');
socket.send('Hello, server!');
};
// Fires every time the server sends data
socket.onmessage = (event) => {
console.log('Received:', event.data);
};
// Fires on network or protocol errors
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Fires when the connection closes (intentionally or not)
socket.onclose = (event) => {
console.log('Disconnected:', event.code, event.reason);
};Four events. That's the entire client-side API.
Sending structured data
Raw WebSocket messages are plain strings. In practice, you'll almost always serialize to JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. so you can send structured data:
// Send a typed message
socket.send(JSON.stringify({
type: 'chat',
room: 'general',
text: 'Hey everyone!'
}));
// Parse incoming messages
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'chat') {
appendMessage(data.user, data.text);
} else if (data.type === 'user-joined') {
showNotification(`${data.user} joined the room`);
}
};Using a type field on every message is a common pattern. It lets you dispatch different kinds of events through a single connection.
Checking connection state before sending
Calling socket.send() on a closed socket throws an error. Always guard with readyState:
function safeSend(socket, data) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data));
} else {
console.warn('Socket not open. Current state:', socket.readyState);
}
}readyState value | Constant | What it means |
|---|---|---|
0 | WebSocket.CONNECTING | Handshake in progress |
1 | WebSocket.OPEN | Ready to send and receive |
2 | WebSocket.CLOSING | Close handshake in progress |
3 | WebSocket.CLOSED | Connection fully closed |
Handling disconnections
Networks are unreliable. Users lose WiFi, phones switch between mobile data and WiFi, servers restart for deployments. A real app needs to detect disconnections and reconnect automatically.
function createConnection(url) {
const socket = new WebSocket(url);
let attempts = 0;
socket.onopen = () => {
attempts = 0;
console.log('Connected!');
};
socket.onclose = () => {
if (attempts < 5) {
attempts++;
const delay = attempts * 2000; // 2s, 4s, 6s, 8s, 10s
console.log(`Reconnecting in ${delay / 1000}s (attempt ${attempts}/5)...`);
setTimeout(() => createConnection(url), delay);
} else {
console.error('Failed to reconnect after 5 attempts.');
}
};
return socket;
}Waiting progressively longer between retries is called exponential backoffWhat is exponential backoff?A retry strategy where each attempt waits twice as long as the previous one, giving an overloaded server progressively more time to recover.. It prevents hundreds of clients from hammering a server the instant it restarts.
delay + Math.random() * 1000, so that clients don't all retry at the exact same moment (called a thundering herd). This is especially important when a server restarts and thousands of users reconnect simultaneously.When to use WebSockets
WebSockets add complexity. You're managing a persistent connection, handling reconnections, and dealing with state. Before reaching for them, ask whether you actually need two-way real-time communication.
| Use WebSockets when... | Stick with HTTP when... |
|---|---|
| Server needs to push updates without being asked | Users trigger every data request |
| Low latency matters (chat, games, collaborative editing) | A few hundred milliseconds of delay is fine |
| Many small messages flow back and forth | Occasional larger requests |
| Real-time dashboards, live feeds, multiplayer | Login forms, product pages, standard APIs |
For cases where only the server pushes data and clients never send anything back, look at Server-Sent EventsWhat is server-sent events?A one-way, server-to-client push mechanism built on plain HTTP - simpler than WebSockets when clients never need to send data back. (SSEWhat is sse?Server-Sent Events - a one-way, server-to-client push mechanism built on plain HTTP, simpler than WebSockets for server push.) first. SSE is plain HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted., works through proxies, and is simpler to implement. WebSockets are the right choice when you genuinely need two-way, real-time communication.
// WebSocket client with automatic reconnection
class RealtimeClient {
constructor(url) {
this.url = url;
this.socket = null;
this.attempts = 0;
this.maxAttempts = 5;
this.listeners = {};
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
this.attempts = 0;
console.log('✅ Connected to', this.url);
this.emit('connect');
};
this.socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.emit('message', data);
if (data.type) {
this.emit(data.type, data);
}
} catch {
this.emit('message', event.data);
}
};
this.socket.onerror = (error) => {
console.error('💥 WebSocket error:', error);
this.emit('error', error);
};
this.socket.onclose = (event) => {
console.log('❌ Disconnected:', event.code, event.reason);
this.emit('disconnect', event);
if (this.attempts < this.maxAttempts) {
this.attempts++;
const delay = this.attempts * 2000 + Math.random() * 500;
console.log(`Reconnecting in ${(delay / 1000).toFixed(1)}s...`);
setTimeout(() => this.connect(), delay);
}
};
}
send(type, payload = {}) {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({ type, ...payload }));
} else {
console.warn('Cannot send: socket is not open');
}
}
on(event, handler) {
if (!this.listeners[event]) this.listeners[event] = [];
this.listeners[event].push(handler);
return this;
}
emit(event, data) {
this.listeners[event]?.forEach(handler => handler(data));
}
disconnect() {
this.maxAttempts = 0;
this.socket?.close();
}
}
// Usage
const client = new RealtimeClient('wss://my-server.com');
client
.on('connect', () => {
client.send('join', { room: 'general' });
})
.on('chat', (data) => {
console.log(`${data.user}: ${data.text}`);
})
.on('user-joined', (data) => {
console.log(`${data.user} joined`);
})
.on('disconnect', () => {
console.log('Lost connection, reconnecting...');
});
client.connect();