Skip to content
Node.js nd libraries 5 min read

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 });
});
ConceptScopeCreated byVisible to client
NamespaceLogical channel over one connectionio.of("/name")Yes — client connects to it
RoomGroup of sockets within a namespacesocket.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.

FeatureRaw WebSocketSocket.IO
Named eventsManual — parse one message streamBuilt in via emit/on
Automatic reconnectionYou implement itBuilt in, with backoff
Transport fallbackWebSocket onlyFalls back to HTTP long-polling
Rooms / broadcastBuild yourselfFirst-class
AcknowledgementsManual correlationCallback argument
Wire formatYour protocolSocket.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 and io.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 after connection.
  • Configure cors.origin explicitly 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.
Last updated June 14, 2026
Was this helpful?