Skip to main content

WebSocket & Socket.IO

HTTP is request-response: the server replies only when the client asks. Instant messages, order notifications, stock prices, or collaboration on the same document need the server to actively push data as soon as it exists โ€” that is the job of WebSocket and libraries such as Socket.IO.

UseExample
๐Ÿ’ฌ ChatSend and receive messages almost instantly, plus a "typing" status
๐ŸŽฎ Online gamesSync the actions of many players
๐Ÿ“ˆ Data monitoringStock prices, a vehicle's position on a map
๐Ÿ”” Live notificationsReceive notifications without reloading the page
๐Ÿค Collaboration toolsSeveral people edit the same document, or draw on the same whiteboard

WebSocket or Socket.IO?โ€‹

WebSocketSocket.IO
What it isA standard protocol (RFC 6455), built into the browserA library built on WebSocket, with its own protocol
CompatibilityAny WebSocket clientOnly a Socket.IO client โ€” a plain new WebSocket() cannot connect
Reconnect, heartbeatYou write itBuilt in
Fallback when WebSocket is blockedNoneHTTP long-polling
Rooms, broadcast, acknowledgementYou write itBuilt in
OverheadLowestA bit higher (packet metadata)
Use whenYou need raw performance, multi-platform clients, a standard protocolYou want the features quickly, and both client and server use Socket.IO
tip

If the server only needs to push one way (notifications, a feed), Server-Sent Events (EventSource) is simpler: it runs on ordinary HTTP, reconnects by itself, and needs no library.

How WebSocket worksโ€‹

  1. Handshake โ€” the client sends an HTTP GET with the header Upgrade: websocket. If the server accepts, it returns 101 Switching Protocols; from then on the TCP connection stays open and switches to the WebSocket protocol.
  2. Data transfer โ€” either side can send at any time (full-duplex), packed into frames. A frame header is only a few bytes (text/binary type, length), much lighter than the header of each HTTP request.
  3. Closing the connection โ€” one side sends a Close frame, the other replies with a Close frame, then TCP closes.

The API built into the browser:

const ws = new WebSocket("wss://example.com/chat"); // always use wss:// (TLS) in production

ws.addEventListener("open", () => ws.send(JSON.stringify({ type: "hello" })));
ws.addEventListener("message", (event) => console.log(JSON.parse(event.data)));
ws.addEventListener("close", (event) => console.log("Closed:", event.code, event.reason));

Plain WebSocket does not reconnect by itself, and it has no rooms or delivery acknowledgement โ€” Socket.IO adds those.

Socket.IOโ€‹

FeatureDescription
Automatic reconnectThe client retries (with backoff) when the network drops
Transport fallbackStarts with HTTP long-polling, then upgrades to WebSocket; still works when a proxy blocks WebSocket
Rooms & namespacesRoom: a group of sockets so you can send to a group (a chat room). Namespace: a separate logical channel on the same connection (/admin, /chat)
BroadcastingSend to everyone, everyone except the sender, or one room โ€” one line
AcknowledgementThe receiver calls a callback to confirm, which gives you a request-response flow over the socket
BufferingThe client buffers emit calls while disconnected and sends them again on reconnect. Server โ†’ client needs connection state recovery turned on (v4.6+)

Emit scopeโ€‹

Rooms in the example: A, B, and D are in room1; C is in room2. A is the client sending the event.

Server: io.emit()
  • Client Aroom1 ยท senderโœ“ receives
  • Client Broom1โœ“ receives
  • Client Croom2โœ“ receives
  • Client Droom1โœ“ receives
Sends to every client.

Acknowledgementโ€‹

// Client: wait for the server to confirm
const response = await socket.timeout(5000).emitWithAck("order:create", { productId: 42 });

// Server
socket.on("order:create", async (payload, callback) => {
const order = await createOrder(payload);
callback({ ok: true, id: order.id });
});

Scaling across serversโ€‹

One server keeps every connection in memory โ€” simple, but limited, and a single point of failure. When you run multiple servers:

  • Load balancer distributes connections across the servers.
  • Sticky session keeps one client on the same server. Required when long-polling is on, because each poll is a separate HTTP request that must reach the server holding the session. If you use only transports: ["websocket"], you do not need it.
  • Redis adapter โ€” user A on server 1 does not receive an io.to(room).emit() called from server 2. The adapter publishes each event through Redis pub/sub so every server forwards it to its own clients.
server.js
import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "redis";

const pubClient = createClient({ url: "redis://localhost:6379" });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);

io.adapter(createAdapter(pubClient, subClient));

Behind Nginx, forward the Upgrade header and turn on sticky sessions:

nginx.conf
upstream socket_nodes {
ip_hash; # sticky session by client IP
server 10.0.1.1:3000;
server 10.0.1.2:3000;
}

server {
location /socket.io/ {
proxy_pass http://socket_nodes;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}

Securityโ€‹

  • Authenticate on connect โ€” the client sends a token in auth; the server checks it with middleware before accepting:

    // Client
    const socket = io("https://api.example.com", { auth: { token } });

    // Server
    io.use(async (socket, next) => {
    try {
    socket.data.user = await verifyJwt(socket.handshake.auth.token);
    next();
    } catch {
    next(new Error("unauthorized"));
    }
    });
  • CORS โ€” allow only the frontend origin (cors: { origin: ["https://app.example.com"] }); do not use "*".

  • Validate payload of every event the same way you validate a request body; do not trust data from the client.

  • Rate-limit events and message size (maxHttpBufferSize) against spam and DoS.

Practiceโ€‹

A minimal chat server:

server.js
import { Server } from "socket.io";

const io = new Server(3000, {
cors: { origin: ["http://localhost:5173"] },
});

io.on("connection", (socket) => {
console.log(`Client connected: ${socket.id}`);
socket.emit("welcome", "Welcome to the chat!");

socket.on("chat message", (msg) => {
// Send to every other client, except the sender
socket.broadcast.emit("chat message", { user: socket.id, message: msg });
});

socket.on("disconnect", () => {
io.emit("user disconnected", socket.id);
});
});
client.js
import { io } from "socket.io-client";

const socket = io("http://localhost:3000");

socket.on("welcome", (text) => console.log(text));
socket.on("chat message", ({ user, message }) => console.log(`${user}: ${message}`));
socket.emit("chat message", "Hello!");

Project ideas to practice:

ProjectWhat you practice
Chat appRooms, an online user list, a "typing" status
Real-time Kanban boardSync card drag-and-drop for everyone watching
Stats dashboardThe server pushes data; charts update without a page reload
Multiplayer game (Pong, Snake)Sync state at a high rate
Notification systemSend to the right user or room when there is a new event
Collaborative whiteboardBroadcast strokes; handle several people acting at once

See alsoโ€‹