Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Socket.IO — User Guide

Generated May 2026 from socketio/socket.io at 439a8f6

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

1. Quick Start

Socket.IO is a real-time, bidirectional, event-based communication library for the web. It enables instant data exchange between a Node.js server and any connected client (browser, mobile app, IoT device, or another server). Socket.IO is NOT a plain WebSocket wrapper — it is a higher-level framework that uses WebSocket as one of its transports while adding a rich feature set on top.

Key Features

  • Automatic transport negotiation — starts with HTTP long-polling, upgrades to WebSocket seamlessly. Falls back gracefully if WebSocket is blocked by proxies or firewalls.
  • Automatic reconnection — clients reconnect with exponential backoff by default; no manual retry logic needed.
  • Rooms — server-side grouping mechanism for broadcasting to subsets of clients (chat rooms, game lobbies, notification channels).
  • Namespaces — multiplexing multiple logical connections over a single underlying transport (e.g., /chat and /admin on one socket).
  • Broadcasting — emit to all clients, all clients in a room, or all clients except specific ones, with a fluent chainable API.
  • Acknowledgements — request-response pattern built into emit; the receiver can call back with data.
  • Binary support — send and receive Buffer, ArrayBuffer, Blob, and File objects natively; the parser handles serialization.
  • Connection state recovery — after a brief disconnection, the server can restore room memberships and replay missed events automatically.

Prerequisites

  • Node.js 16 or later (LTS recommended)
  • npm 7+ or yarn 1.22+
  • A modern browser (Chrome, Firefox, Safari, Edge) for client-side usage

Minimal Install

mkdir my-realtime-app && cd my-realtime-app
npm init -y
npm install socket.io socket.io-client

Minimal Example

Server (server.js):

const { Server } = require("socket.io");

const io = new Server(3000, {
  cors: { origin: "*" }
});

io.on("connection", (socket) => {
  console.log(`Client connected: ${socket.id}`);

  socket.on("hello", (msg) => {
    console.log(`Received: ${msg}`);
    socket.emit("hello", `Echo: ${msg}`);
  });

  socket.on("disconnect", (reason) => {
    console.log(`Client disconnected: ${reason}`);
  });
});

console.log("Socket.IO server listening on port 3000");

Client (client.js):

const { io } = require("socket.io-client");

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

socket.on("connect", () => {
  console.log(`Connected as ${socket.id}`);
  socket.emit("hello", "world");
});

socket.on("hello", (msg) => {
  console.log(`Server says: ${msg}`);
});

Run it:

node server.js &
node client.js

Expected output:

Socket.IO server listening on port 3000
Client connected: abc123XYZ
Connected as abc123XYZ
Received: world
Server says: Echo: world

Tip

You do not need to set up Express or any HTTP framework to get started. The Server constructor can create its own HTTP server internally when you pass a port number directly.

What to Try Next

After verifying the minimal example works, explore Section 4 (Core Usage) for rooms, namespaces, and broadcasting. Section 5 has 25+ task-oriented recipes covering chat apps, authentication, TypeScript, and more.


2. Installation

Server:

npm install socket.io

Client (Node.js or bundler):

npm install socket.io-client

Both together (for full-stack projects):

npm install socket.io socket.io-client

Important

The server package (socket.io) and the client package (socket.io-client) are versioned independently but designed to work together within the same major version. Always use matching major versions (e.g., both v4.x). Mixing v3 server with v4 client will cause silent connection failures.

CDN for Browser Usage

For quick prototyping or static HTML pages, load the client from a CDN:

<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
<script>
  const socket = io("http://localhost:3000");
  socket.on("connect", () => console.log("Connected!"));
</script>

Alternatively, when serveClient is enabled on the server (the default), the server itself serves the client bundle at the configured path:

<script src="/socket.io/socket.io.js"></script>

ES Module Import (Browser)

<script type="module">
  import { io } from "https://cdn.socket.io/4.8.1/socket.io.esm.min.js";
  const socket = io("http://localhost:3000");
</script>

From Source

git clone https://github.com/socketio/socket.io.git
cd socket.io
npm install
npm run build

The monorepo uses npm workspaces. Building from source compiles all 12 packages (server, client, engine.io, parsers, adapters, etc.).

System Requirements

Requirement Minimum Recommended
Node.js 16.x 20.x LTS or later
npm 7.x 10.x
Browser Any modern (ES6+) Latest Chrome/Firefox/Safari/Edge
OS Any that runs Node.js Linux for production servers

Post-Install Verification

Server check:

const { Server } = require("socket.io");
console.log("socket.io loaded successfully");
const io = new Server();
console.log(`Server instance created, path: ${io.path()}`);
io.close();

Client check:

const { io } = require("socket.io-client");
console.log("socket.io-client loaded successfully");
const socket = io("http://localhost:1", { autoConnect: false });
console.log(`Socket created, connected: ${socket.connected}`);

Run each with node -e or as a script. If no errors appear, the packages are installed correctly.


3. Configuration

This section covers the user-facing configuration options you need for day-to-day development. For production tuning, adapter configuration, cluster setup, and security hardening, see the Admin Guide.

Server Options

When creating a Server instance, pass an options object as the second argument (or the only argument if not attaching to an existing HTTP server):

const { Server } = require("socket.io");

const io = new Server(httpServer, {
  path: "/socket.io",
  serveClient: true,
  connectTimeout: 45000,
  cors: {
    origin: "https://myapp.com",
    methods: ["GET", "POST"]
  }
});

Socket.IO Server Options:

Option Type Default Description
path string "/socket.io" The path the server listens on. Must match the client's path option.
serveClient boolean true Whether to serve the client JS bundle at {path}/socket.io.js.
adapter constructor in-memory Adapter The adapter class for managing rooms. Swap for Redis/Postgres adapter in multi-server deployments.
parser object socket.io-parser Custom parser for encoding/decoding packets.
connectTimeout number 45000 Milliseconds to wait for a client to join a namespace before disconnecting it.
connectionStateRecovery object undefined Enable connection state recovery (see Section 4).
cleanupEmptyChildNamespaces boolean false Automatically remove dynamic child namespaces when all clients disconnect.

Engine.IO Options (passed through to the transport layer):

Option Type Default Description
pingTimeout number 20000 Ms without a pong before considering the connection closed.
pingInterval number 25000 Ms between ping packets.
upgradeTimeout number 10000 Ms to complete a transport upgrade (polling to WebSocket).
maxHttpBufferSize number 1000000 Maximum packet size in bytes (1 MB). Rejects larger payloads.
transports string[] ["polling", "websocket"] Allowed transports. Add "webtransport" to enable it.
allowUpgrades boolean true Whether to allow transport upgrades (polling to WebSocket).
perMessageDeflate boolean/object false Enable WebSocket per-message compression.
httpCompression boolean/object true Enable compression for HTTP long-polling responses.
cors object — CORS configuration (passed to the cors npm package).
cookie object/boolean false Enable and configure Engine.IO session cookies.
allowEIO3 boolean false Allow Engine.IO v3 clients to connect (backward compatibility).

Client Options

The client io() factory accepts a URL and an options object:

const { io } = require("socket.io-client");

const socket = io("https://myserver.com", {
  path: "/socket.io",
  reconnection: true,
  reconnectionAttempts: Infinity,
  reconnectionDelay: 1000,
  timeout: 20000,
  auth: { token: "my-jwt-token" }
});

Manager Options (connection-level):

Option Type Default Description
path string "/socket.io" Must match the server's path.
forceNew boolean false Force a new Manager (new connection) instead of reusing.
multiplex boolean true Enable connection multiplexing for multiple namespaces.
reconnection boolean true Enable automatic reconnection.
reconnectionAttempts number Infinity Max reconnection attempts before giving up.
reconnectionDelay number 1000 Initial delay (ms) before first reconnection attempt.
reconnectionDelayMax number 5000 Maximum delay (ms) between reconnection attempts.
randomizationFactor number 0.5 Jitter factor (0-1) applied to reconnection delay.
timeout number 20000 Connection timeout (ms) before a connect_error is emitted.
autoConnect boolean true Connect automatically on creation. Set false to connect manually.

Socket Options (passed alongside Manager options):

Option Type Default Description
auth object/function — Authentication payload sent during handshake.
transports string[] ["polling", "websocket"] Transports to use.
query object — Query parameters appended to the connection URL.
extraHeaders object — Additional HTTP headers (only for polling and WebSocket in Node.js).

Matching Server and Client Configuration

The path option must be identical on both sides. If you change it on the server, change it on every client:

// Server
const io = new Server(httpServer, { path: "/my-custom-path" });

// Client
const socket = io("https://myserver.com", { path: "/my-custom-path" });

If they do not match, the client will receive 404 errors and never connect.


4. Core Usage

4.1 Connecting

Client-Side Connection

The io() function creates a socket connection:

const { io } = require("socket.io-client");

// Connect to the same host (browser) or specify URL (Node.js)
const socket = io("http://localhost:3000");

// Connect to a specific namespace
const adminSocket = io("http://localhost:3000/admin");

// Connect with options
const socket = io("http://localhost:3000", {
  auth: { token: "abc123" },
  transports: ["websocket"],
  autoConnect: false
});

// Manually connect when autoConnect is false
socket.connect();

Connection Events

socket.on("connect", () => {
  console.log("Connected:", socket.id);
});

socket.on("connect_error", (err) => {
  console.log("Connection error:", err.message);
});

socket.on("disconnect", (reason, details) => {
  console.log("Disconnected:", reason);
  // reason can be:
  //   "io server disconnect" — server called socket.disconnect()
  //   "io client disconnect" — client called socket.disconnect()
  //   "ping timeout" — server didn't respond in time
  //   "transport close" — connection was lost
  //   "transport error" — connection encountered an error
  //   "parse error" — received invalid packet
  //   "forced close" — client called manager.close()
});

Note

When the server forcefully disconnects a socket with socket.disconnect(true), the client receives "io server disconnect" and will NOT automatically reconnect. You must explicitly call socket.connect() if you want to reconnect.

Manager Events

The underlying Manager handles the transport connection and exposes its own events:

socket.io.on("open", () => console.log("Transport opened"));
socket.io.on("close", (reason) => console.log("Transport closed:", reason));
socket.io.on("ping", () => console.log("Ping from server"));
socket.io.on("reconnect", (attempt) => console.log(`Reconnected after ${attempt} attempts`));
socket.io.on("reconnect_attempt", (attempt) => console.log(`Reconnect attempt #${attempt}`));
socket.io.on("reconnect_error", (err) => console.log("Reconnect error:", err));
socket.io.on("reconnect_failed", () => console.log("Reconnect failed — gave up"));

Server-Side Connection Handling

io.on("connection", (socket) => {
  console.log("New client:", socket.id);
  console.log("Auth data:", socket.handshake.auth);
  console.log("Query params:", socket.handshake.query);
  console.log("Headers:", socket.handshake.headers);
  console.log("Remote address:", socket.handshake.address);

  socket.on("disconnect", (reason) => {
    console.log(`Client ${socket.id} disconnected: ${reason}`);
  });
});

4.2 Emitting Events

Socket.IO uses a publish-subscribe model. Any string can be an event name (except reserved names like connect, disconnect, error).

Basic Emit

// Client to server
socket.emit("chat message", "Hello, world!");

// Server to client
socket.emit("chat message", "Welcome!");

// Multiple arguments
socket.emit("user:update", { name: "Alice" }, [1, 2, 3], true);

Emit with Acknowledgement (Callback)

The sender passes a callback as the last argument. The receiver invokes it to send a response:

// Client sends, expects response
socket.emit("save:document", { title: "Draft" }, (response) => {
  console.log("Server acknowledged:", response);
});

// Server handles, responds
socket.on("save:document", (doc, callback) => {
  const savedId = saveToDb(doc);
  callback({ status: "ok", id: savedId });
});

Emit with Ack and Timeout

socket.timeout(5000).emit("save:document", { title: "Draft" }, (err, response) => {
  if (err) {
    console.log("Server did not acknowledge in time");
  } else {
    console.log("Saved:", response);
  }
});

Promise-Based Emit (emitWithAck)

// Client
try {
  const response = await socket.emitWithAck("save:document", { title: "Draft" });
  console.log("Response:", response);
} catch (err) {
  console.log("Timeout or error:", err);
}

// Server handler remains the same — just call the callback
socket.on("save:document", (doc, callback) => {
  callback({ status: "ok" });
});

Warning

When using emitWithAck, always set a timeout with socket.timeout(ms).emitWithAck(...). Without a timeout, the promise will hang forever if the other side never acknowledges. This was identified as a memory leak vector in production environments.

4.3 Listening for Events

Standard Listeners

// Listen for a specific event
socket.on("chat message", (msg) => {
  console.log("Message:", msg);
});

// Listen once (auto-removes after first call)
socket.once("welcome", (data) => {
  console.log("Welcome message:", data);
});

// Remove a specific listener
const handler = (msg) => console.log(msg);
socket.on("chat message", handler);
socket.off("chat message", handler);

// Remove all listeners for an event
socket.removeAllListeners("chat message");

Catch-All Listeners

// Listen to every incoming event
socket.onAny((eventName, ...args) => {
  console.log(`Event: ${eventName}`, args);
});

// Listen to every outgoing event
socket.onAnyOutgoing((eventName, ...args) => {
  console.log(`Sending: ${eventName}`, args);
});

// Remove catch-all listeners
socket.offAny();
socket.offAnyOutgoing();

Prepend Listeners

// Add listener to beginning of the listener array
socket.prependAny((eventName, ...args) => {
  console.log("First handler for any event:", eventName);
});

4.4 Rooms

Rooms are a server-side concept. They are arbitrary channels that sockets can join and leave. Rooms exist only on the server — clients do not know which rooms they are in.

Joining and Leaving

io.on("connection", (socket) => {
  // Join a room
  socket.join("room1");

  // Join multiple rooms
  socket.join(["room1", "room2", "room3"]);

  // Leave a room
  socket.leave("room1");

  // Every socket automatically joins a room named after its socket.id
  console.log(socket.rooms); // Set { socket.id, "room1", "room2", ... }
});

Broadcasting to Rooms

// Send to everyone in "room1" (including sender if in the room)
io.to("room1").emit("event", data);

// Same as to() — in() is an alias
io.in("room1").emit("event", data);

// Send to multiple rooms (union)
io.to("room1").to("room2").emit("event", data);

// Send to "room1" but exclude "room2"
io.to("room1").except("room2").emit("event", data);

// From within a socket handler: broadcast to room EXCLUDING the sender
socket.to("room1").emit("event", data);

Listing Sockets in a Room

// Get all socket IDs in a room
const sockets = await io.in("room1").fetchSockets();
for (const s of sockets) {
  console.log(s.id, s.data, s.rooms);
}

Bulk Room Operations

// Make all sockets in "room1" also join "room2"
io.in("room1").socketsJoin("room2");

// Make all sockets in "room1" leave "room2"
io.in("room1").socketsLeave("room2");

// Disconnect all sockets in "room1"
io.in("room1").disconnectSockets(true);

Tip

Use the socket's own ID as a "personal room" to send messages to a specific user. Every socket automatically joins a room matching its socket.id, so io.to(targetSocketId).emit("private", data) works out of the box. For persistent identity across reconnections, use a user-specific room (see Section 5 recipes).

4.5 Namespaces

Namespaces let you split your application logic over a single shared connection. They act as separate communication channels with their own events, rooms, and middleware.

Creating Namespaces (Server)

const { Server } = require("socket.io");
const io = new Server(3000);

// Main namespace (default)
io.on("connection", (socket) => {
  console.log("Main namespace:", socket.id);
});

// Custom namespace
const adminNsp = io.of("/admin");
adminNsp.on("connection", (socket) => {
  console.log("Admin namespace:", socket.id);
});

// Dynamic namespaces (regex)
const dynamicNsp = io.of(/^\/user-\w+$/);
dynamicNsp.on("connection", (socket) => {
  const namespace = socket.nsp.name; // e.g., "/user-alice"
  console.log(`Dynamic namespace ${namespace}:`, socket.id);
});

// Dynamic namespaces (function)
io.of((name, auth, next) => {
  // Validate and authorize
  if (name.startsWith("/project-")) {
    next(null, true);
  } else {
    next(new Error("Invalid namespace"));
  }
}).on("connection", (socket) => {
  console.log("Project namespace:", socket.nsp.name);
});

Connecting to Namespaces (Client)

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

// Custom namespace
const adminSocket = io("http://localhost:3000/admin");

// Dynamic namespace
const userSocket = io("http://localhost:3000/user-alice");

When multiplexing is enabled (default), all namespace connections to the same server share a single underlying transport connection.

Namespace Middleware

adminNsp.use((socket, next) => {
  const token = socket.handshake.auth.token;
  if (isValidAdmin(token)) {
    next();
  } else {
    next(new Error("Unauthorized"));
  }
});

4.6 Broadcasting

The server provides a fluent, chainable API for targeting specific recipients.

Broadcast Targets

// To ALL connected clients (current namespace)
io.emit("announcement", "Server is restarting");

// To all EXCEPT the sender
socket.broadcast.emit("user:joined", socket.id);

// To a specific room
io.to("room1").emit("event", data);

// To multiple rooms (clients in room1 OR room2)
io.to("room1").to("room2").emit("event", data);

// Exclude specific rooms
io.except("room1").emit("event", data);
io.to("room2").except("room3").emit("event", data);

Broadcast Modifiers

// Volatile — drop the message if the client is not ready to receive
io.volatile.emit("cursor:move", { x: 100, y: 200 });

// With compression
io.compress(true).emit("large:data", bigPayload);

// Local — only broadcast to clients on THIS server (skip adapter broadcast in clusters)
io.local.emit("event", data);

// Timeout — expect acknowledgement from all recipients within a time window
io.timeout(5000).emit("important:event", data, (err, responses) => {
  if (err) {
    // Some clients did not acknowledge
  } else {
    // responses is an array of all acknowledgements
  }
});

Chain Everything Together

io.to("room1")
  .to("room2")
  .except("room3")
  .compress(true)
  .timeout(5000)
  .emit("complex:broadcast", data, (err, responses) => {
    console.log(`${responses.length} clients acknowledged`);
  });

emitWithAck for Broadcasts

try {
  const responses = await io.timeout(5000).emitWithAck("ping");
  console.log(`${responses.length} clients responded`);
} catch (err) {
  console.log("Some clients did not respond in time");
}

4.7 Binary Data

Socket.IO natively supports binary data. The parser automatically detects binary content and handles serialization.

// Server sends a Buffer
const buffer = Buffer.from("Hello binary world");
socket.emit("file:chunk", buffer);

// Client sends an ArrayBuffer (browser)
const arrayBuffer = new ArrayBuffer(8);
const view = new Uint8Array(arrayBuffer);
view.set([1, 2, 3, 4, 5, 6, 7, 8]);
socket.emit("binary:data", arrayBuffer);

// Mixed: object with binary inside
socket.emit("upload", {
  filename: "photo.jpg",
  mimetype: "image/jpeg",
  data: fileBuffer
});

// The parser handles nested binary in arrays and objects automatically
socket.emit("complex", {
  text: "metadata",
  files: [buffer1, buffer2],
  nested: { deep: { binary: buffer3 } }
});

Note

Binary payloads are extracted from the packet, sent as separate WebSocket frames, and reassembled on the receiving end. This is transparent to your code. The parser enforces attachment limits — if a packet claims more binary attachments than actually arrive, you will see a "too many attachments" parser error.

4.8 Error Handling

Client-Side Errors

// Connection error (failed to connect or lost during reconnection)
socket.on("connect_error", (err) => {
  console.error("Connection error:", err.message);
  // err.message contains the error reason
  // Common: CORS, auth failure, server down
});

// Disconnection
socket.on("disconnect", (reason) => {
  if (reason === "io server disconnect") {
    // The server explicitly disconnected this socket
    // Must manually reconnect
    socket.connect();
  }
  // Otherwise, the client will auto-reconnect
});

Server-Side Errors

io.on("connection", (socket) => {
  // Catch errors on individual sockets
  socket.on("error", (err) => {
    console.error(`Socket ${socket.id} error:`, err);
  });

  // Wrap event handlers with try/catch
  socket.on("risky:operation", async (data, callback) => {
    try {
      const result = await doSomethingRisky(data);
      callback({ status: "ok", result });
    } catch (err) {
      callback({ status: "error", message: err.message });
    }
  });
});

// Engine-level errors
io.engine.on("connection_error", (err) => {
  console.log("Engine error:", err.code, err.message, err.context);
});

Middleware Errors

When middleware calls next(err), the client receives a connect_error event:

// Server middleware
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  if (!token) {
    return next(new Error("Authentication required"));
  }
  next();
});

// Client handling
socket.on("connect_error", (err) => {
  console.log("Auth failed:", err.message); // "Authentication required"
});

5. Common Tasks

5.1 Chat Application

// Server
const { Server } = require("socket.io");
const io = new Server(3000, { cors: { origin: "*" } });

io.on("connection", (socket) => {
  socket.on("join room", (room) => {
    socket.join(room);
    socket.to(room).emit("system", `${socket.id} joined the room`);
  });

  socket.on("chat message", ({ room, message }) => {
    io.to(room).emit("chat message", {
      from: socket.id,
      message,
      timestamp: Date.now()
    });
  });

  socket.on("typing", (room) => {
    socket.to(room).emit("typing", socket.id);
  });

  socket.on("disconnect", () => {
    // Socket automatically leaves all rooms on disconnect
  });
});
// Client
const socket = io("http://localhost:3000");

socket.emit("join room", "general");

socket.on("chat message", ({ from, message, timestamp }) => {
  console.log(`[${new Date(timestamp).toLocaleTimeString()}] ${from}: ${message}`);
});

socket.on("system", (msg) => console.log(`*** ${msg} ***`));

socket.emit("chat message", { room: "general", message: "Hello everyone!" });

5.2 Real-Time Notifications

// Server — push notifications to specific users
const userSockets = new Map(); // userId -> Set<socketId>

io.on("connection", (socket) => {
  const userId = socket.handshake.auth.userId;
  socket.join(`user:${userId}`);

  if (!userSockets.has(userId)) userSockets.set(userId, new Set());
  userSockets.get(userId).add(socket.id);

  socket.on("disconnect", () => {
    userSockets.get(userId)?.delete(socket.id);
    if (userSockets.get(userId)?.size === 0) userSockets.delete(userId);
  });
});

// Called from your application logic (e.g., after a DB write)
function notifyUser(userId, notification) {
  io.to(`user:${userId}`).emit("notification", notification);
}

5.3 Room-Based Multiplayer Game

// Server
const games = new Map();

io.on("connection", (socket) => {
  socket.on("game:create", (gameId, callback) => {
    games.set(gameId, { players: [socket.id], state: {} });
    socket.join(`game:${gameId}`);
    callback({ status: "ok", gameId });
  });

  socket.on("game:join", (gameId, callback) => {
    const game = games.get(gameId);
    if (!game) return callback({ status: "error", message: "Game not found" });
    if (game.players.length >= 4) return callback({ status: "error", message: "Game full" });

    game.players.push(socket.id);
    socket.join(`game:${gameId}`);
    io.to(`game:${gameId}`).emit("game:player_joined", {
      playerId: socket.id,
      playerCount: game.players.length
    });
    callback({ status: "ok" });
  });

  socket.on("game:move", ({ gameId, move }) => {
    const game = games.get(gameId);
    if (!game) return;
    // Validate and apply move
    game.state = applyMove(game.state, move);
    io.to(`game:${gameId}`).emit("game:state", game.state);
  });

  socket.on("disconnect", () => {
    for (const [gameId, game] of games) {
      const idx = game.players.indexOf(socket.id);
      if (idx !== -1) {
        game.players.splice(idx, 1);
        io.to(`game:${gameId}`).emit("game:player_left", socket.id);
        if (game.players.length === 0) games.delete(gameId);
      }
    }
  });
});

5.4 Private Messaging

// Server — using user-specific rooms for persistence across reconnections
io.use((socket, next) => {
  const userId = socket.handshake.auth.userId;
  if (!userId) return next(new Error("userId required"));
  socket.data.userId = userId;
  next();
});

io.on("connection", (socket) => {
  // Join a personal room keyed by user ID (survives reconnection with new socket.id)
  socket.join(`user:${socket.data.userId}`);

  socket.on("private message", ({ to, content }) => {
    const message = {
      from: socket.data.userId,
      to,
      content,
      timestamp: Date.now()
    };
    // Send to recipient
    io.to(`user:${to}`).emit("private message", message);
    // Send back to sender (for multi-device sync)
    socket.emit("private message", message);
  });
});

5.5 File Upload Progress

// Client (browser)
async function uploadFile(file) {
  const chunkSize = 64 * 1024; // 64 KB chunks
  const totalChunks = Math.ceil(file.size / chunkSize);

  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    const buffer = await chunk.arrayBuffer();

    await socket.timeout(10000).emitWithAck("file:chunk", {
      name: file.name,
      chunk: buffer,
      index: i,
      total: totalChunks
    });

    console.log(`Progress: ${Math.round(((i + 1) / totalChunks) * 100)}%`);
  }
}
// Server
const chunks = new Map();

io.on("connection", (socket) => {
  socket.on("file:chunk", ({ name, chunk, index, total }, callback) => {
    const key = `${socket.id}:${name}`;
    if (!chunks.has(key)) chunks.set(key, []);

    chunks.get(key)[index] = Buffer.from(chunk);

    if (chunks.get(key).filter(Boolean).length === total) {
      const fullFile = Buffer.concat(chunks.get(key));
      saveFile(name, fullFile);
      chunks.delete(key);
      callback({ status: "complete" });
    } else {
      callback({ status: "ok" });
    }
  });
});

5.6 Connection State Recovery

Connection state recovery allows the server to restore a client's room memberships and replay missed events after a brief disconnection, without any application-level logic.

// Server — enable connection state recovery
const io = new Server(3000, {
  connectionStateRecovery: {
    maxDisconnectionDuration: 2 * 60 * 1000, // 2 minutes (default)
    skipMiddlewares: true                      // skip auth on recovery (default)
  }
});

io.on("connection", (socket) => {
  if (socket.recovered) {
    // Connection was recovered — rooms are restored, missed events replayed
    console.log("Client recovered:", socket.id);
  } else {
    // Fresh connection — set up from scratch
    console.log("New client:", socket.id);
    socket.join("default-room");
  }
});
// Client — no special configuration needed, just handle reconnection
socket.on("connect", () => {
  if (socket.recovered) {
    console.log("Session recovered — rooms and missed events restored");
  } else {
    console.log("New session — re-subscribe to data");
  }
});

Important

Connection state recovery only works within the maxDisconnectionDuration window (default 2 minutes). After that, the server discards the recovery state. The feature relies on the in-memory adapter by default; for multi-server deployments, you need the cluster adapter.

5.7 Custom Parser

Socket.IO supports custom parsers for performance optimization or alternative serialization formats. The msgpack parser is a popular choice for reducing payload size.

// Install
// npm install @socket.io/msgpack-parser

// Server
const { Server } = require("socket.io");
const msgpackParser = require("@socket.io/msgpack-parser");

const io = new Server(3000, {
  parser: msgpackParser
});

// Client
const { io } = require("socket.io-client");
const msgpackParser = require("@socket.io/msgpack-parser");

const socket = io("http://localhost:3000", {
  parser: msgpackParser
});

Both server and client MUST use the same parser. Mismatched parsers will cause "parse error" disconnections.

5.8 TypeScript — Typed Events

Socket.IO has first-class TypeScript support with typed event maps:

// shared/types.ts — define event maps
interface ServerToClientEvents {
  "chat:message": (msg: { from: string; body: string; timestamp: number }) => void;
  "user:joined": (userId: string) => void;
  "user:left": (userId: string) => void;
}

interface ClientToServerEvents {
  "chat:send": (body: string, callback: (status: "ok" | "error") => void) => void;
  "room:join": (roomId: string) => void;
}

interface InterServerEvents {
  ping: () => void;
}

interface SocketData {
  userId: string;
  username: string;
}
// server.ts
import { Server } from "socket.io";

const io = new Server<
  ClientToServerEvents,
  ServerToClientEvents,
  InterServerEvents,
  SocketData
>(3000);

io.on("connection", (socket) => {
  // socket.data is typed as SocketData
  socket.data.userId = "abc";

  // Event handlers are fully typed
  socket.on("chat:send", (body, callback) => {
    io.emit("chat:message", {
      from: socket.data.userId,
      body,
      timestamp: Date.now()
    });
    callback("ok");
  });

  // TypeScript errors if you emit an undefined event or wrong argument types
  // socket.emit("nonexistent", 123); // TS error!
});
// client.ts
import { io, Socket } from "socket.io-client";

const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io("http://localhost:3000");

socket.on("chat:message", (msg) => {
  // msg is typed as { from: string; body: string; timestamp: number }
  console.log(`${msg.from}: ${msg.body}`);
});

socket.emit("chat:send", "Hello!", (status) => {
  // status is typed as "ok" | "error"
  console.log(status);
});

5.9 Using with Express

const express = require("express");
const { createServer } = require("http");
const { Server } = require("socket.io");

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
  cors: { origin: "*" }
});

// Express routes
app.get("/", (req, res) => {
  res.sendFile(__dirname + "/index.html");
});

// Socket.IO events
io.on("connection", (socket) => {
  console.log("Client connected");
});

// IMPORTANT: call httpServer.listen(), NOT app.listen()
httpServer.listen(3000, () => {
  console.log("Server running on port 3000");
});

5.10 Using with Koa

const Koa = require("koa");
const { createServer } = require("http");
const { Server } = require("socket.io");

const app = new Koa();
const httpServer = createServer(app.callback());
const io = new Server(httpServer);

io.on("connection", (socket) => {
  console.log("Client connected");
});

httpServer.listen(3000);

5.11 Using with NestJS

The Socket.IO monorepo includes a NestJS example. The pattern uses NestJS WebSocket gateways:

import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect
} from "@nestjs/websockets";
import { Server, Socket } from "socket.io";

@WebSocketGateway({ cors: { origin: "*" } })
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;

  handleConnection(client: Socket) {
    console.log("Client connected:", client.id);
  }

  handleDisconnect(client: Socket) {
    console.log("Client disconnected:", client.id);
  }

  @SubscribeMessage("message")
  handleMessage(client: Socket, payload: string): void {
    this.server.emit("message", payload);
  }
}

5.12 Authentication with Middleware

const jwt = require("jsonwebtoken");

// Server-side middleware
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  if (!token) {
    return next(new Error("Authentication required"));
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    socket.data.user = decoded;
    next();
  } catch (err) {
    next(new Error("Invalid token"));
  }
});

io.on("connection", (socket) => {
  console.log("Authenticated user:", socket.data.user.name);
});
// Client-side
const socket = io("http://localhost:3000", {
  auth: {
    token: localStorage.getItem("jwt")
  }
});

socket.on("connect_error", (err) => {
  if (err.message === "Authentication required" || err.message === "Invalid token") {
    // Redirect to login
    window.location.href = "/login";
  }
});

For dynamic tokens that may refresh between reconnection attempts, pass a function:

const socket = io("http://localhost:3000", {
  auth: (cb) => {
    cb({ token: getLatestToken() });
  }
});

5.13 Reconnection Handling

const socket = io("http://localhost:3000", {
  reconnection: true,
  reconnectionAttempts: 10,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 5000,
  randomizationFactor: 0.5
});

socket.io.on("reconnect_attempt", (attempt) => {
  console.log(`Reconnection attempt ${attempt}/10`);
  // Update UI: "Reconnecting..."
});

socket.io.on("reconnect", (attempt) => {
  console.log(`Reconnected after ${attempt} attempts`);
  // Re-fetch state, re-subscribe
  socket.emit("sync:state");
});

socket.io.on("reconnect_failed", () => {
  console.log("Failed to reconnect after all attempts");
  // Show "Connection lost" UI with manual retry button
});

// Manual reconnect button
document.getElementById("retry").addEventListener("click", () => {
  socket.connect();
});

Tip

The randomizationFactor (default 0.5) adds jitter to prevent a "thundering herd" when all clients reconnect simultaneously after a server restart. A value of 0.5 means the actual delay will be between 50% and 150% of the computed delay. Do not set this to 0 in production.

5.14 Broadcasting to Specific Users

When users can have multiple tabs or devices connected, use a user-keyed room instead of socket.id:

io.use((socket, next) => {
  socket.data.userId = socket.handshake.auth.userId;
  next();
});

io.on("connection", (socket) => {
  // All of this user's sockets join the same room
  socket.join(`user:${socket.data.userId}`);
});

// Send to a specific user (all their devices/tabs)
function sendToUser(userId, event, data) {
  io.to(`user:${userId}`).emit(event, data);
}

// Check if a user is online
async function isUserOnline(userId) {
  const sockets = await io.in(`user:${userId}`).fetchSockets();
  return sockets.length > 0;
}

5.15 Monitoring Connection Count

// Simple counter
let connectionCount = 0;

io.on("connection", (socket) => {
  connectionCount++;
  console.log(`Connections: ${connectionCount}`);

  socket.on("disconnect", () => {
    connectionCount--;
    console.log(`Connections: ${connectionCount}`);
  });
});

// Using the adapter (more accurate, works across clusters)
setInterval(async () => {
  const sockets = await io.fetchSockets();
  console.log(`Active sockets: ${sockets.length}`);
}, 30000);

// Per-room counts
async function getRoomSize(room) {
  const sockets = await io.in(room).fetchSockets();
  return sockets.length;
}

5.16 Cross-Server Communication (serverSideEmit)

In a multi-server deployment (with Redis or Postgres adapter), serverSideEmit lets servers communicate directly:

// Server A sends to all other servers
io.serverSideEmit("deploy:started", { version: "2.0.0" });

// Server B listens
io.on("deploy:started", ({ version }) => {
  console.log(`Deploy started: ${version}`);
  // Warm caches, prepare for restart, etc.
});

// With acknowledgement — every server responds
try {
  const responses = await io.serverSideEmitWithAck("stats:request");
  // responses is an array, one per server
  const totalConnections = responses.reduce((sum, r) => sum + r.connections, 0);
  console.log(`Total connections across cluster: ${totalConnections}`);
} catch (err) {
  console.log("Not all servers responded");
}

5.17 Using with CORS

CORS is the most common source of connection failures when the client and server are on different origins.

// Allow a single origin
const io = new Server(3000, {
  cors: {
    origin: "https://myapp.com"
  }
});

// Allow multiple origins
const io = new Server(3000, {
  cors: {
    origin: ["https://myapp.com", "https://staging.myapp.com"]
  }
});

// Allow all origins (development only)
const io = new Server(3000, {
  cors: {
    origin: "*"
  }
});

// Full CORS configuration
const io = new Server(3000, {
  cors: {
    origin: "https://myapp.com",
    methods: ["GET", "POST"],
    allowedHeaders: ["Authorization"],
    credentials: true
  }
});

Caution

Never use cors: { origin: "*" } in production. It allows any website to connect to your Socket.IO server. Always specify the exact origins that should be allowed. If you need credentials (cookies, auth headers), you must specify explicit origins — "*" does not work with credentials: true.

5.18 Volatile Events

Volatile events are dropped if the client is not ready to receive them. Useful for data that becomes stale quickly, like cursor positions or live scores:

// Server
setInterval(() => {
  io.volatile.emit("stock:price", {
    symbol: "AAPL",
    price: getCurrentPrice("AAPL"),
    timestamp: Date.now()
  });
}, 100);

// Client — if processing is slow, some ticks are simply skipped
socket.on("stock:price", (data) => {
  updateChart(data);
});

5.19 Event Timeout Patterns

// Client: emit with timeout
socket.timeout(5000).emit("save:data", payload, (err, response) => {
  if (err) {
    showError("Server did not respond in time");
  } else {
    showSuccess("Saved!");
  }
});

// Server: broadcast with ack timeout
io.to("room1").timeout(3000).emit("poll:question", question, (err, responses) => {
  if (err) {
    console.log("Some clients did not respond");
  }
  console.log(`${responses.length} responses collected`);
});

// Async/await version
try {
  const response = await socket.timeout(5000).emitWithAck("save:data", payload);
  console.log("Response:", response);
} catch (err) {
  console.log("Timeout:", err.message);
}

5.20 Using socket.data for Per-Socket State

io.on("connection", (socket) => {
  // Attach arbitrary data to the socket
  socket.data.username = socket.handshake.auth.username;
  socket.data.role = "user";
  socket.data.joinedAt = Date.now();

  // socket.data is included when fetching remote sockets
  socket.on("promote", async (targetId) => {
    const [target] = await io.in(targetId).fetchSockets();
    if (target) {
      target.data.role = "moderator";
    }
  });
});

5.21 Multiple Manager Instances

By default, io() reuses the same Manager for connections to the same host (multiplexing). Force separate connections when needed:

// These share one Manager (one transport connection)
const chatSocket = io("http://localhost:3000/chat");
const feedSocket = io("http://localhost:3000/feed");

// Force a separate connection
const isolatedSocket = io("http://localhost:3000/isolated", {
  forceNew: true
});

// Access the shared manager
chatSocket.io === feedSocket.io; // true
chatSocket.io === isolatedSocket.io; // false

5.22 Handling Middleware Rejection Gracefully

// Server
io.use((socket, next) => {
  if (!socket.handshake.auth.token) {
    const err = new Error("AUTH_REQUIRED");
    err.data = { hint: "Provide auth.token" };
    return next(err);
  }
  next();
});

// Client
socket.on("connect_error", (err) => {
  console.log("Error:", err.message);   // "AUTH_REQUIRED"
  console.log("Details:", err.data);     // { hint: "Provide auth.token" }

  // Update auth and retry
  socket.auth = { token: promptUserForToken() };
  socket.connect();
});

5.23 Catch-All Event Logging (Debug/Development)

// Server — log all incoming events
io.on("connection", (socket) => {
  socket.onAny((event, ...args) => {
    console.log(`[${socket.id}] >>> ${event}`, args);
  });

  socket.onAnyOutgoing((event, ...args) => {
    console.log(`[${socket.id}] <<< ${event}`, args);
  });
});

// Client
socket.onAny((event, ...args) => {
  console.log(`Server event: ${event}`, args);
});

5.24 Graceful Server Shutdown

process.on("SIGTERM", () => {
  console.log("SIGTERM received, shutting down...");

  // Notify all clients
  io.emit("server:shutdown", { reason: "maintenance", reconnectIn: 30 });

  // Close the server (stops accepting new connections)
  io.close(() => {
    console.log("All connections closed");
    process.exit(0);
  });
});

5.25 WebTransport Support

WebTransport is a newer transport that offers lower latency than WebSocket. It must be explicitly enabled:

// Server
const { Server } = require("socket.io");

const io = new Server(3000, {
  transports: ["polling", "websocket", "webtransport"]
});

// Client (browser only, requires HTTPS)
const socket = io("https://myserver.com", {
  transports: ["websocket", "webtransport"]
});

// Check which transport is active
socket.on("connect", () => {
  console.log("Transport:", socket.io.engine.transport.name);
});

WebTransport requires HTTPS and a browser that supports the WebTransport API (Chrome 97+).

5.26 Room Events via Adapter

Track when rooms are created and destroyed:

io.of("/").adapter.on("create-room", (room) => {
  console.log(`Room created: ${room}`);
});

io.of("/").adapter.on("delete-room", (room) => {
  console.log(`Room deleted: ${room}`);
});

io.of("/").adapter.on("join-room", (room, id) => {
  console.log(`Socket ${id} joined room ${room}`);
});

io.of("/").adapter.on("leave-room", (room, id) => {
  console.log(`Socket ${id} left room ${room}`);
});

5.27 Using Query Parameters

// Client — pass data as query params (visible in URL, use auth for secrets)
const socket = io("http://localhost:3000", {
  query: {
    version: "2.0",
    platform: "web"
  }
});

// Server — read query params
io.on("connection", (socket) => {
  console.log("Version:", socket.handshake.query.version);
  console.log("Platform:", socket.handshake.query.platform);
});

5.28 Namespace-Level Middleware Chains

const adminNsp = io.of("/admin");

// Middleware 1: authentication
adminNsp.use((socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    socket.data.user = jwt.verify(token, SECRET);
    next();
  } catch {
    next(new Error("Invalid token"));
  }
});

// Middleware 2: authorization
adminNsp.use((socket, next) => {
  if (socket.data.user.role !== "admin") {
    return next(new Error("Admin role required"));
  }
  next();
});

// Middleware 3: rate limiting
const connectionCounts = new Map();
adminNsp.use((socket, next) => {
  const ip = socket.handshake.address;
  const count = connectionCounts.get(ip) || 0;
  if (count >= 5) return next(new Error("Too many connections"));
  connectionCounts.set(ip, count + 1);
  next();
});

adminNsp.on("connection", (socket) => {
  console.log(`Admin connected: ${socket.data.user.name}`);
});

5.29 Disconnecting Event (Before Rooms Are Left)

The disconnecting event fires before the socket leaves its rooms, allowing you to broadcast a farewell or inspect room membership one last time:

io.on("connection", (socket) => {
  socket.on("disconnecting", (reason) => {
    // socket.rooms is still populated at this point
    for (const room of socket.rooms) {
      if (room !== socket.id) {
        socket.to(room).emit("user:leaving", {
          userId: socket.data.userId,
          room
        });
      }
    }
  });

  socket.on("disconnect", (reason) => {
    // socket.rooms is empty at this point
  });
});

5.30 Rate Limiting Events

Prevent clients from flooding the server with events:

io.on("connection", (socket) => {
  const rateLimiter = {
    tokens: 10,
    maxTokens: 10,
    lastRefill: Date.now(),
    refillRate: 1000 // 1 token per second
  };

  socket.use(([event, ...args], next) => {
    const now = Date.now();
    const elapsed = now - rateLimiter.lastRefill;
    rateLimiter.tokens = Math.min(
      rateLimiter.maxTokens,
      rateLimiter.tokens + (elapsed / rateLimiter.refillRate)
    );
    rateLimiter.lastRefill = now;

    if (rateLimiter.tokens < 1) {
      return next(new Error("Rate limit exceeded"));
    }

    rateLimiter.tokens--;
    next();
  });

  socket.on("error", (err) => {
    if (err.message === "Rate limit exceeded") {
      socket.emit("error:rate_limit", "Slow down! Too many events.");
    }
  });
});

6. Troubleshooting

6.1 CORS Errors

Symptom: Browser console shows Access to XMLHttpRequest ... has been blocked by CORS policy.

Cause: The Socket.IO server does not have CORS configured, or the configured origin does not match.

Fix:

const io = new Server(httpServer, {
  cors: {
    origin: "https://your-frontend-domain.com",
    methods: ["GET", "POST"],
    credentials: true
  }
});

If you are using cookies or credentials: true on the client, you cannot use origin: "*". You must specify the exact origin.

6.2 Firefox CORS Issues

Symptom: Connection works in Chrome but fails in Firefox with CORS errors. Related to GitHub issue #5288.

Cause: Firefox has stricter CORS enforcement in certain cases, especially with credentialed requests.

Fix: Ensure your CORS configuration includes credentials: true and an explicit (not wildcard) origin. Double-check that the Access-Control-Allow-Origin response header matches your page's origin exactly, including the protocol and port.

6.3 Connection Timeout

Symptom: Client emits connect_error after 20 seconds with a timeout message.

Cause: Server is unreachable, wrong URL, or firewall blocking.

Fix:

  1. Verify the server is running and accessible: curl http://localhost:3000/socket.io/?EIO=4&transport=polling
  2. Ensure the path option matches on client and server (default: "/socket.io")
  3. Check firewall rules
  4. Increase timeout if the server is slow: io("url", { timeout: 60000 })

6.4 Reconnection Not Working After Server Disconnect

Symptom: After the server calls socket.disconnect(), the client does not reconnect. Related to GitHub issue #5197.

Cause: When the server forcefully disconnects a socket, the client receives the "io server disconnect" reason and intentionally does NOT auto-reconnect (by design).

Fix:

socket.on("disconnect", (reason) => {
  if (reason === "io server disconnect") {
    // Must manually reconnect
    socket.connect();
  }
  // All other reasons auto-reconnect if reconnection is enabled
});

6.5 emitWithAck Memory Leak

Symptom: Memory usage grows over time when using emitWithAck or acknowledgement callbacks. Related to GitHub issue #4984.

Cause: If the receiver never calls the acknowledgement callback, the sender retains the callback in memory indefinitely.

Fix: Always use .timeout() before emitWithAck:

// This can leak if the server never responds
const response = await socket.emitWithAck("event", data); // BAD

// This will reject after the timeout
const response = await socket.timeout(10000).emitWithAck("event", data); // GOOD

6.6 mTLS Connection Errors

Symptom: Connection fails with TLS errors when using mutual TLS (client certificates). Related to GitHub issue #5480.

Cause: The Engine.IO client creates separate TLS contexts for polling and WebSocket transports, and client certificates may not be forwarded correctly across upgrades.

Fix: Pass TLS options at the engine level:

const socket = io("https://secure-server.com", {
  ca: fs.readFileSync("ca.pem"),
  cert: fs.readFileSync("client-cert.pem"),
  key: fs.readFileSync("client-key.pem"),
  rejectUnauthorized: true
});

6.7 Timer Reliability Issues

Symptom: Ping/pong timeouts fire incorrectly, causing unexpected disconnections. Related to GitHub issue #5357.

Cause: On some platforms (especially mobile browsers or Node.js with heavy event loop lag), timers may fire late, causing the server to believe the client has disconnected.

Fix: Increase pingTimeout on the server:

const io = new Server(httpServer, {
  pingTimeout: 60000,  // 60 seconds instead of default 20
  pingInterval: 25000
});

6.8 ESM Placeholder Bug

Symptom: Importing Socket.IO with ESM (import) fails or imports an empty/placeholder module. Related to GitHub issue #5215.

Cause: Some bundler or module resolution configurations pick up a placeholder ESM entry point instead of the real module.

Fix: Ensure your package.json has "type": "module" if using ESM, or use the explicit import path:

import { Server } from "socket.io/dist/index.js";

Alternatively, use createRequire as a workaround in ESM files:

import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { Server } = require("socket.io");

6.9 CloudFlare Proxy Issues

Symptom: Connections drop after 100 seconds or WebSocket upgrades fail. Related to GitHub issue #2580.

Cause: CloudFlare terminates idle WebSocket connections after 100 seconds and may buffer/modify polling responses.

Fix:

  • Reduce the pingInterval below 100 seconds (it is 25 seconds by default, which should be fine)
  • Ensure CloudFlare's WebSocket support is enabled in the dashboard
  • If using long-polling behind CloudFlare, be aware that CloudFlare may buffer chunked responses; consider starting with WebSocket only: transports: ["websocket"]

6.10 Parser Error: "illegal attachments"

Symptom: Connection drops with the error "illegal attachments".

Cause: A packet declares binary attachments but the number of received attachments does not match the declared count. This can happen with corrupted packets, proxy interference, or mismatched parsers.

Fix: Ensure client and server use the same parser. If using a custom parser, verify it correctly handles binary attachment counts.

6.11 Parser Error: "too many attachments"

Symptom: The parser rejects a packet with "too many attachments".

Cause: A packet declares an unusually high number of binary attachments, possibly from a malicious client.

Fix: This is a protection mechanism. Reduce maxHttpBufferSize to limit overall packet size. If legitimate, check that your serialization logic is not creating excessive attachments from deeply nested objects.

6.12 Parser Error: "unknown type"

Symptom: Parser error with "unknown type: X" where X is a number.

Cause: The packet type byte does not match any known Socket.IO packet type (0-6). This typically means a protocol version mismatch or corrupted data.

Fix: Ensure both client and server are on compatible versions. If connecting a v3 client to a v4 server, enable allowEIO3: true.

6.13 Parser Error: "invalid payload"

Symptom: Malformed packet error during parsing.

Cause: The JSON payload portion of a Socket.IO packet is not valid JSON.

Fix: Check for proxy or CDN interference that may modify response bodies. Ensure httpCompression settings are compatible with your proxy.

6.14 Parser Error: "got plaintext data when reconstructing a packet"

Symptom: This error appears during binary packet reconstruction.

Cause: The parser expects binary frames for attachments but received a text frame instead. This can happen when a proxy converts binary WebSocket frames to text.

Fix: Check your proxy/load balancer configuration for WebSocket frame handling. Some proxies do not properly forward binary WebSocket frames.

6.15 Rooms Not Receiving Events

Symptom: Events emitted to a room are not received by any client.

Cause: Multiple possibilities — the client never joined the room, the room name has a typo, or the join happened asynchronously after the emit.

Fix:

// Verify the socket is actually in the room
io.on("connection", (socket) => {
  socket.join("room1");
  console.log("Rooms:", [...socket.rooms]); // Should include "room1"
});

// Check room membership before emitting
const sockets = await io.in("room1").fetchSockets();
console.log(`Sockets in room1: ${sockets.length}`);

6.16 Namespace Connection Denied

Symptom: Client receives connect_error when connecting to a namespace.

Cause: Namespace middleware rejected the connection, or the namespace does not exist and no catch-all dynamic namespace is configured.

Fix: Check the server error message:

// Client
socket.on("connect_error", (err) => {
  console.log(err.message); // The error from next(new Error(...))
});

Ensure the namespace is registered on the server before clients try to connect.

6.17 Transport Upgrade Failures

Symptom: Connection stays on polling and never upgrades to WebSocket.

Cause: A reverse proxy, load balancer, or firewall is not configured to handle WebSocket upgrade requests.

Fix:

For nginx:

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

For Apache, enable mod_proxy_wstunnel. For AWS ALB, ensure WebSocket support is enabled (it is by default on Application Load Balancers).

6.18 Client Receives Events Meant for Others

Symptom: A client receives events that should have been sent to a different socket or room.

Cause: Using io.emit() instead of socket.to(room).emit(), or accidentally joining sockets to wrong rooms.

Fix: Audit your emit calls. io.emit() sends to ALL connected sockets. Use io.to(room).emit() or socket.to(room).emit() for targeted delivery.

6.19 Multiple Connections from Same Client

Symptom: The server logs multiple connection events from the same browser tab.

Cause: Calling io() multiple times without multiplex (the default reuses the Manager, but calling io() for the same namespace twice creates two sockets). Or React/Vue component mounting twice in development mode.

Fix:

// Singleton pattern
let socket;
function getSocket() {
  if (!socket) {
    socket = io("http://localhost:3000");
  }
  return socket;
}

In React with Strict Mode, components mount twice in development. Create the socket outside the component or use a ref.

6.20 Events Received Out of Order

Symptom: Events arrive at the client in a different order than they were emitted by the server.

Cause: If the connection upgrades from polling to WebSocket mid-stream, some events may be in-flight on the old transport while new events go through the new one.

Fix: Socket.IO guarantees ordering within a single transport. During the brief upgrade window, both transports run in parallel, and Engine.IO buffers to maintain ordering. If you are seeing out-of-order delivery, check for:

  • Application logic emitting from different async contexts
  • Multiple server instances without a shared adapter

6.21 High Memory Usage

Symptom: Server memory grows unboundedly.

Cause: Common causes include: accumulated event listeners (memory leak), unacknowledged callbacks, rooms never cleaned up, or buffered packets for disconnected sockets.

Fix:

  • Use socket.removeAllListeners() on disconnect if you add many listeners
  • Always use .timeout() with acknowledgements
  • Enable cleanupEmptyChildNamespaces: true if using dynamic namespaces
  • Monitor with process.memoryUsage() and the fetchSockets() count

6.22 Packet Size Exceeded

Symptom: Large events silently fail or the connection drops.

Cause: The payload exceeds maxHttpBufferSize (default 1 MB).

Fix:

const io = new Server(httpServer, {
  maxHttpBufferSize: 5e6 // 5 MB
});

For very large payloads, chunk the data (see Section 5.5 for file upload pattern).

Warning

Increasing maxHttpBufferSize too much exposes your server to denial-of-service attacks. A single malicious client could send enormous payloads that consume all server memory. Use chunking for legitimately large data transfers instead.

6.23 "websocket" Transport Not Available in Node.js Client

Symptom: Client falls back to polling when you expect WebSocket.

Cause: The ws (or bufferutil/utf-8-validate) packages may not be installed, or native add-ons failed to compile.

Fix:

npm install ws bufferutil utf-8-validate

If native compilation fails, Socket.IO falls back to pure JS WebSocket which is functional but slower. The connection will still work — just without native performance optimizations.

6.24 Disconnect Fires Immediately After Connect

Symptom: Client connects and immediately disconnects.

Cause: The connectTimeout on the server (default 45 seconds) closes sockets that connect at the Engine.IO level but never join a namespace. If your client connects and then takes a long time before the Socket.IO handshake completes (e.g., heavy middleware), the server may time out.

Fix: Increase connectTimeout or optimize middleware:

const io = new Server(httpServer, {
  connectTimeout: 60000
});

6.25 Events Not Reaching All Servers in a Cluster

Symptom: Events emitted with io.emit() only reach clients connected to the originating server.

Cause: You are running multiple server instances without a shared adapter (Redis, Postgres, or cluster adapter).

Fix: Install and configure the Redis streams adapter or Postgres adapter:

const { Server } = require("socket.io");
const { createAdapter } = require("@socket.io/redis-streams-adapter");
const { createClient } = require("redis");

const pubClient = createClient({ url: "redis://localhost:6379" });
await pubClient.connect();

const io = new Server(httpServer, {
  adapter: createAdapter(pubClient)
});

6.26 Debug Logging

Socket.IO uses the debug module for internal logging. Enable it to see what is happening under the hood:

# Enable all Socket.IO debug output
DEBUG=socket.io*,engine.io* node server.js

# Enable only client debug
DEBUG=socket.io-client* node client.js

# Enable only engine-level transport debug
DEBUG=engine.io* node server.js

Tip

In the browser, set localStorage.debug = 'socket.io-client:*' and reload the page to see client-side debug output in the developer console.

6.27 Connection Works Locally But Not in Production

Symptom: Everything works on localhost, but fails behind a reverse proxy.

Cause: The proxy needs to forward both polling (HTTP) and WebSocket upgrade requests, and the path must match.

Fix checklist:

  1. Proxy forwards the correct path (/socket.io/ by default)
  2. Proxy supports WebSocket upgrades (Upgrade and Connection headers)
  3. Proxy does not aggressively timeout idle connections (increase timeout to at least pingInterval + pingTimeout)
  4. Sticky sessions are configured if running multiple server instances behind a load balancer (required for polling transport)

6.28 "Invalid namespace" Error

Symptom: Client receives "Invalid namespace" in connect_error.

Cause: The client is trying to connect to a namespace (e.g., /admin) that has not been registered on the server with io.of("/admin").

Fix: Register the namespace on the server before any client connects:

// Must exist BEFORE clients connect
const adminNsp = io.of("/admin");
adminNsp.on("connection", (socket) => { /* ... */ });

Or use a dynamic namespace with a regex or function validator to accept connections to unknown namespaces.


7. Frequently Asked Questions

7.1 Is Socket.IO a WebSocket library?

No. Socket.IO is a higher-level abstraction that uses WebSocket as one of its underlying transports. It adds rooms, namespaces, automatic reconnection, broadcasting, acknowledgements, and fallback transports (HTTP long-polling). A plain WebSocket client cannot connect to a Socket.IO server — the protocol is different.

7.2 Can I connect a plain WebSocket client to a Socket.IO server?

No. Socket.IO uses its own packet protocol (Engine.IO at the transport level, Socket.IO parser at the application level). You must use a Socket.IO client library. Socket.IO clients are available for JavaScript, Java, Swift, Dart, Python, C++, and Rust.

7.3 What is Engine.IO?

Engine.IO is the low-level transport layer that Socket.IO builds upon. It handles the actual connection (polling, WebSocket, WebTransport), heartbeat (ping/pong), and transport upgrades. You rarely interact with Engine.IO directly — Socket.IO wraps it.

7.4 What is the difference between io.emit() and socket.emit()?

io.emit(event, data) sends the event to ALL connected clients in the default namespace. socket.emit(event, data) sends the event to ONE specific client (the socket). For broadcasting to everyone EXCEPT the sender, use socket.broadcast.emit(event, data).

7.5 What is the difference between to() and in()?

They are aliases — io.to("room1") and io.in("room1") are identical.

7.6 Do rooms exist on the client?

No. Rooms are a server-side concept only. Clients do not know which rooms they are in. The server manages room membership and uses it to determine broadcast targets.

7.7 How do I send a message to a specific user?

Use a user-specific room. When the user connects, have the socket join a room keyed by user ID: socket.join("user:" + userId). Then send to that user with io.to("user:" + userId).emit(...). This works across multiple tabs/devices.

7.8 What happens when the client loses connection?

By default, the client automatically attempts to reconnect with exponential backoff (starting at 1 second, maxing at 5 seconds, with jitter). The client will retry indefinitely unless reconnectionAttempts is set to a finite number.

7.9 What is connection state recovery?

A feature that allows the server to restore a client's room memberships and replay missed events after a brief disconnection (up to maxDisconnectionDuration, default 2 minutes). It must be explicitly enabled on the server.

7.10 Why does the connection start with polling?

This is by design. Starting with HTTP long-polling ensures the connection succeeds even in restrictive network environments (corporate firewalls, proxies that block WebSocket). Once the polling connection is established, Socket.IO upgrades to WebSocket transparently for better performance. To skip polling and start with WebSocket directly: io("url", { transports: ["websocket"] }).

7.11 How do I skip the polling phase and go straight to WebSocket?

const socket = io("http://localhost:3000", {
  transports: ["websocket"]
});

This is faster but removes the fallback — if WebSocket is blocked, the connection will fail entirely.

7.12 What is the path option?

The path (default "/socket.io") is the URL path the server listens on and the client connects to. All Socket.IO HTTP requests (polling, WebSocket upgrade) go through this path. Must match on both sides. This is useful when running multiple Socket.IO servers on the same host or behind a reverse proxy with path-based routing.

7.13 Can I use Socket.IO without Express?

Yes. The Server constructor can create its own HTTP server when you pass a port number. Express, Koa, or any HTTP framework is optional.

7.14 What is the maximum message size?

Controlled by maxHttpBufferSize, which defaults to 1 MB (1,000,000 bytes). Messages exceeding this limit cause the connection to close. Increase it carefully.

7.15 How do I handle authentication?

Use the auth option on the client and a use() middleware on the server. The auth data is available in socket.handshake.auth on the server. See Section 5.12 for a complete JWT example.

7.16 Can I use Socket.IO in a cluster (multiple servers)?

Yes, but you need a shared adapter (e.g., @socket.io/redis-streams-adapter, @socket.io/postgres-adapter, or @socket.io/cluster-adapter). Without one, events only reach clients connected to the emitting server. Sticky sessions are also required if using the polling transport.

7.17 What are sticky sessions and why are they needed?

Sticky sessions ensure that all HTTP requests from a single client reach the same server instance. This is required for the polling transport because it uses multiple HTTP requests that must all hit the same server. Sticky sessions are NOT needed if you only use the WebSocket transport.

7.18 What is the volatile flag?

Marking an emit as volatile tells Socket.IO that the message can be dropped if the client is not ready to receive it (e.g., if the internal buffer is full or the client is reconnecting). Useful for high-frequency, ephemeral data like cursor positions or live metrics.

7.19 What is the difference between disconnect() and close()?

socket.disconnect() disconnects a single socket. io.close() shuts down the entire server and all connections. On the client, socket.disconnect() cleanly closes the connection (the client will NOT auto-reconnect).

7.20 How do I broadcast to everyone except the sender?

socket.broadcast.emit("event", data);
// or
socket.to("room").emit("event", data); // to a room, excluding sender

7.21 What is fetchSockets() and when should I use it?

fetchSockets() returns an array of matching socket instances (or remote socket descriptors in a cluster). Use it to inspect socket state, get connection counts, or perform bulk operations:

const sockets = await io.fetchSockets();
console.log(`${sockets.length} clients connected`);

for (const s of sockets) {
  console.log(s.id, s.rooms, s.data);
}

7.22 What is serverSideEmit?

It allows servers in a multi-server deployment to communicate directly with each other (not with clients). Requires a shared adapter. Useful for coordination like cache invalidation or deployment signaling.

7.23 Does Socket.IO support WebTransport?

Yes, as of recent versions. WebTransport must be explicitly added to the transports array (["polling", "websocket", "webtransport"]). It requires HTTPS and is currently supported only in Chromium-based browsers.

7.24 How do I debug connection issues?

Enable debug logging with DEBUG=socket.io*,engine.io* (Node.js) or localStorage.debug = 'socket.io-client:*' (browser). This shows every transport event, packet exchange, and error.

7.25 What are the reserved event names I cannot use?

On both client and server, the following event names are reserved and cannot be used for application events: connect, connect_error, disconnect, disconnecting, newListener, removeListener. Emitting these names will throw an error or be silently consumed by the framework.

7.26 How do namespaces differ from rooms?

Namespaces create separate communication channels — each has its own connection event, middleware chain, and rooms. Clients explicitly connect to a namespace. Rooms are server-side groupings within a namespace — clients do not know about them. Use namespaces to separate application concerns (e.g., /chat vs /admin). Use rooms to group users within a namespace (e.g., "room:lobby", "room:game-123").

7.27 Can I use Socket.IO with TypeScript?

Yes. Socket.IO has built-in TypeScript definitions and supports typed event maps for full type safety on both client and server. See Section 5.8 for a complete example.

7.28 What happens to messages when a client is disconnected?

They are lost. Socket.IO does not persist or queue messages for offline clients by default. If you need guaranteed delivery, implement application-level message persistence (store in a database, deliver on reconnect) or use connection state recovery for short disconnections.

7.29 What is the compress flag?

io.compress(true).emit(...) enables per-message compression for that emit. For WebSocket, this uses the permessage-deflate extension. For polling, it uses HTTP compression. Useful for large payloads; adds CPU overhead for small ones.

7.30 How do I handle CORS?

Pass a cors object in the server options. The options are passed directly to the cors npm package. See Section 5.17 for examples. CORS only applies to cross-origin connections — same-origin connections are unaffected.

7.31 What is the disconnecting event?

The disconnecting event fires on the server BEFORE the socket leaves its rooms. This lets you inspect socket.rooms and broadcast a "user is leaving" message to rooms before the socket is fully removed. The disconnect event fires after rooms are cleared.

7.32 What is socket.handshake?

The handshake object contains metadata from the initial connection: headers, query, auth, address, time, url, secure, and xdomain. It is read-only and fixed for the lifetime of the socket.

7.33 Can I emit events from outside the connection handler?

Yes. If you store a reference to the io server instance, you can emit from anywhere — HTTP route handlers, timers, database event listeners:

// Express route that triggers a Socket.IO event
app.post("/api/notifications", (req, res) => {
  io.to(`user:${req.body.userId}`).emit("notification", req.body.message);
  res.json({ sent: true });
});

7.34 What is the local flag?

io.local.emit(...) broadcasts only to clients connected to the current server instance, skipping the adapter's cross-server broadcast. Useful for server-specific events in a cluster.

7.35 How many concurrent connections can Socket.IO handle?

This depends on your hardware, OS settings (file descriptor limits), and workload. A single Node.js process can typically handle 10,000-50,000 concurrent WebSocket connections. For higher scale, use clustering with a shared adapter.

7.36 What packages are in the Socket.IO monorepo?

The monorepo contains 12 packages: socket.io (server), socket.io-client, engine.io (server transport), engine.io-client, engine.io-parser, socket.io-parser, socket.io-adapter (base adapter), socket.io-cluster-adapter, socket.io-cluster-engine, socket.io-postgres-emitter, socket.io-redis-streams-emitter, and socket.io-component-emitter.

7.37 What examples are included in the repository?

The repository includes example projects: angular-todomvc, basic-crud-application, basic-websocket-client, custom-parsers, nestjs-example, and private-messaging. Each demonstrates a specific use case with both server and client code.

7.38 What is the except() method?

io.except("room1").emit(...) broadcasts to all sockets EXCEPT those in room1. It can be chained with to(): io.to("room2").except("room1").emit(...) sends to sockets in room2 but not in room1.

7.39 Can I use a custom ID instead of the auto-generated socket.id?

Not directly. The socket.id is generated by the server and cannot be overridden. To track users by custom identity, use socket.data to store your custom ID and a user-keyed room for addressing.

7.40 What is the difference between emitWithAck and emit with a callback?

emitWithAck returns a Promise, making it usable with async/await. A regular emit with a callback uses the traditional callback pattern. They do the same thing — emitWithAck is syntactic sugar that wraps the callback in a Promise.