Socket.IO: Real-Time Communication
Socket.IO is a library for building real-time, bidirectional, event-based communication between a Node.js server and its clients. Where the request/response model of HTTP forces the client to ask before it can learn anything, Socket.IO keeps a persistent connection open so either side can push data the instant something happens — chat messages, live dashboards, multiplayer game state, collaborative editing. It builds on the WebSocket protocol but adds automatic reconnection, transport fallbacks, and a friendly event API on top. This page covers server and client setup, emitting and listening to events, rooms and namespaces, broadcasting, and how Socket.IO differs from raw WebSockets.
Setting up the server
Socket.IO runs on any maintained Node.js release; Node 20 or 22 LTS is the sensible default. Install the server package and attach a Socket.IO instance to an HTTP server — the same server that, in a real app, would also serve your Express or Fastify routes.
npm install socket.io
The Server constructor wraps a Node http.Server. The connection event fires once per client; the socket it hands you represents that single client’s connection and is where you register per-client listeners.
import { createServer } from "node:http";
import { Server } from "socket.io";
const httpServer = createServer();
const io = new Server(httpServer, {
cors: { origin: "http://localhost:5173" },
});
io.on("connection", (socket) => {
console.log(`client connected: ${socket.id}`);
socket.on("disconnect", (reason) => {
console.log(`client gone: ${socket.id} (${reason})`);
});
});
httpServer.listen(3000, () => console.log("listening on :3000"));
Output:
listening on :3000
client connected: 8Yvq2pT1nQ3kZ0AAAAB
CommonJS works identically with const { Server } = require("socket.io"). The cors option is required when the browser client is served from a different origin than the Socket.IO endpoint.
Setting up the client
The browser client connects with the socket.io-client package; Node processes can use it too. Calling io(url) opens the connection and immediately tries to upgrade to a WebSocket transport.
npm install socket.io-client
import { io } from "socket.io-client";
const socket = io("http://localhost:3000");
socket.on("connect", () => {
console.log("connected as", socket.id);
socket.emit("chat:message", { text: "hello" });
});
Emitting and listening to events
Communication is symmetric: both sides call emit(eventName, payload) to send and on(eventName, handler) to receive. Event names are arbitrary strings — namespacing them with a feature:action convention keeps large apps legible. Payloads are serialized for you, so plain objects, arrays, strings, and numbers all pass through cleanly.
// server
io.on("connection", (socket) => {
socket.on("chat:message", (msg) => {
console.log("received:", msg.text);
// echo back only to this client
socket.emit("chat:ack", { received: true, at: Date.now() });
});
});
For request/response style flows, the sender can pass a callback as the final argument and the receiver invokes it as an acknowledgement — Socket.IO routes the reply back to the exact caller.
// client asks, server answers
socket.emit("user:fetch", { id: 42 }, (response) => {
console.log(response.name); // "Ada"
});
// server
socket.on("user:fetch", (query, ack) => {
ack({ id: query.id, name: "Ada" });
});
Never trust an event payload. Anything the client emits is user input — validate it on the server (for example with Joi or Zod) before acting on it.
Broadcasting
A plain socket.emit reaches only that one client. To reach others, Socket.IO offers several targets. io.emit sends to every connected client; socket.broadcast.emit sends to everyone except the sender — the usual choice for “user X is typing” or “a new message arrived” notifications.
io.on("connection", (socket) => {
// tell everyone else someone joined
socket.broadcast.emit("presence:join", { id: socket.id });
socket.on("chat:message", (msg) => {
// fan the message out to all other clients
socket.broadcast.emit("chat:message", { from: socket.id, ...msg });
});
});
Rooms and namespaces
A room is a server-side grouping of sockets you can broadcast to as a unit — a chat channel, a document, a game lobby. Sockets join and leave rooms with socket.join and socket.leave, and you emit to a room by name. Clients are never aware of rooms directly; they are purely a server-side routing tool.
io.on("connection", (socket) => {
socket.on("room:join", (roomId) => {
socket.join(roomId);
// emit to everyone in the room except the joiner
socket.to(roomId).emit("room:notice", `${socket.id} joined`);
});
socket.on("room:message", ({ roomId, text }) => {
// io.to includes the sender; socket.to excludes it
io.to(roomId).emit("room:message", { from: socket.id, text });
});
});
A namespace is a separate communication channel multiplexed over the same physical connection — useful for splitting major concerns like /chat and /admin, each with its own middleware and events. Rooms exist inside a namespace.
const adminNs = io.of("/admin");
adminNs.on("connection", (socket) => {
socket.emit("admin:welcome", { ok: true });
});
| Concept | Scope | Created by | Visible to client |
|---|---|---|---|
| Namespace | Logical channel over one connection | io.of("/name") | Yes — client connects to it |
| Room | Group of sockets within a namespace | socket.join("id") | No — server-side only |
How it differs from raw WebSockets
The native WebSocket API gives you a raw, persistent byte/text channel and nothing else. Socket.IO is a higher-level protocol layered on top of WebSocket (and other transports), adding the features production real-time apps almost always end up needing.
| Feature | Raw WebSocket | Socket.IO |
|---|---|---|
| Named events | Manual — parse one message stream | Built in via emit/on |
| Automatic reconnection | You implement it | Built in, with backoff |
| Transport fallback | WebSocket only | Falls back to HTTP long-polling |
| Rooms / broadcast | Build yourself | First-class |
| Acknowledgements | Manual correlation | Callback argument |
| Wire format | Your protocol | Socket.IO’s own (not WS-compatible) |
The key caveat: because Socket.IO speaks its own protocol, a Socket.IO client cannot talk to a bare WebSocket server, and vice versa. If you need to interoperate with a plain WebSocket peer, use the ws library instead. If you control both ends and want events, rooms, and resilient reconnection out of the box, Socket.IO is the productive choice.
Best practices
- Validate every inbound event payload on the server — treat it as untrusted user input.
- Namespace event names (
chat:message,presence:join) so intent is obvious as the app grows. - Use
socket.to(room)to exclude the sender andio.to(room)to include everyone when broadcasting. - Prefer rooms over tracking socket IDs by hand; let Socket.IO manage membership and cleanup on disconnect.
- Authenticate during the handshake with namespace middleware (
io.use(...)) rather than afterconnection. - Configure
cors.originexplicitly to your front-end origins; never leave it wide open in production. - For multiple server instances, add the Redis adapter so rooms and broadcasts work across the cluster.