NexusCS

Socket.io

JavaScript
Quick reference for Socket.io - a library for real-time, bidirectional, event-based communication between browsers and servers using WebSockets.
featured

Getting started

Server setup (Node.js)

// ES6 import
import { Server } from "socket.io";

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

// Standalone server
const io = new Server(3000);

// With Express
const app = require("express")();
const server = require("http").createServer(app);
const io = new Server(server);
server.listen(3000);

Client setup (Browser)

<!-- CDN -->
<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io();
</script>
// ES6 module
import { io } from "socket.io-client";
const socket = io("http://localhost:3000");

// CommonJS
const io = require("socket.io-client");
const socket = io("http://localhost:3000");

Basic connection

// Server
io.on("connection", (socket) => {
  console.log("User connected:", socket.id);

  socket.on("disconnect", () => {
    console.log("User disconnected");
  });
});

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

Emitting events

Server to client

// To single client
socket.emit("message", "Hello client");

// To all clients
io.emit("broadcast", "Hello everyone");

// To all except sender
socket.broadcast.emit("news", "New user joined");

// To specific room
io.to("room1").emit("roomMsg", "Hello room");

// To multiple rooms
io.to("room1").to("room2").emit("multi", "data");

// With acknowledgement callback
socket.emit("question", "data", (answer) => {
  console.log(answer);
});

Client to server

// Basic emit
socket.emit("message", "Hello server");

// With multiple arguments
socket.emit("chat", "username", "message text");

// With object
socket.emit("user", { name: "John", age: 30 });

// With callback (acknowledgement)
socket.emit("question", "data", (response) => {
  console.log("Server response:", response);
});

Listening to events

Server-side listeners

io.on("connection", (socket) => {
  // Single event
  socket.on("message", (data) => {
    console.log("Received:", data);
  });

  // With acknowledgement
  socket.on("question", (data, callback) => {
    console.log(data);
    callback("Answer from server");
  });

  // Multiple arguments
  socket.on("chat", (user, msg, time) => {
    console.log(`${user} at ${time}: ${msg}`);
  });

  // Catch-all listener
  socket.onAny((eventName, ...args) => {
    console.log(`Event: ${eventName}`, args);
  });
});

Client-side listeners

// Basic listener
socket.on("message", (data) => {
  console.log("Received:", data);
});

// With acknowledgement
socket.on("question", (data, callback) => {
  console.log(data);
  callback("Answer from client");
});

// Connection events
socket.on("connect", () => {
  console.log("Connected");
});

socket.on("disconnect", () => {
  console.log("Disconnected");
});

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

Rooms

Joining and leaving

// Join room
socket.join("room1");

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

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

// Join room conditionally
io.on("connection", (socket) => {
  socket.on("joinRoom", (room) => {
    socket.join(room);
    socket.emit("joined", `You joined ${room}`);
  });
});

Broadcasting to rooms

// To specific room
io.to("room1").emit("message", "Hello room1");

// To multiple rooms
io.to("room1").to("room2").emit("news", "data");

// To room except sender
socket.to("room1").emit("message", "Others in room");

// Get all sockets in room
const sockets = await io.in("room1").fetchSockets();
console.log("Room size:", sockets.length);

// Get all rooms
const rooms = Array.from(socket.rooms);
console.log("Socket rooms:", rooms);

Namespaces

Creating namespaces

// Server: Create namespace
const chatNs = io.of("/chat");
const adminNs = io.of("/admin");

chatNs.on("connection", (socket) => {
  console.log("Chat namespace connected");
  socket.emit("welcome", "Welcome to chat");
});

adminNs.on("connection", (socket) => {
  console.log("Admin namespace connected");
});

// Default namespace
io.of("/").on("connection", (socket) => {
  // Main namespace
});

Client connecting to namespaces

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

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

// Dynamic namespace
const dynamicNs = io(`http://localhost:3000/${namespace}`);

chatSocket.on("connect", () => {
  console.log("Connected to chat namespace");
});

Broadcasting patterns

Broadcast types

// To all clients
io.emit("event", "data");

// To all except sender
socket.broadcast.emit("event", "data");

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

// To room except sender
socket.to("room1").emit("event", "data");

// To specific socket ID
io.to(socketId).emit("private", "data");

// To all in namespace
io.of("/chat").emit("event", "data");

Advanced broadcasting

// Volatile (ok if lost)
socket.volatile.emit("tick", Date.now());

// Compressed
socket.compress(true).emit("large", bigData);

// Binary data
socket.emit("image", buffer);

// To local clients only (no Redis broadcast)
io.local.emit("event", "data");

// Chain modifiers
socket.to("room1").compress(true).volatile.emit("fast", "data");

Targeting specific clients

// Get socket by ID
const targetSocket = io.sockets.sockets.get(socketId);
if (targetSocket) {
  targetSocket.emit("private", "message");
}

// Broadcast to specific socket IDs
io.to([socketId1, socketId2]).emit("group", "data");

// Exclude specific sockets
socket.broadcast.except(socketId).emit("public", "data");

Acknowledgements

Server requesting acknowledgement

// Server sends with callback
io.on("connection", (socket) => {
  socket.emit("question", "data", (response) => {
    console.log("Client responded:", response);
  });

  // With timeout (v4.6+)
  socket.timeout(5000).emit("request", "data", (err, response) => {
    if (err) {
      console.error("Timeout or error");
    } else {
      console.log("Response:", response);
    }
  });
});

Client responding

// Client receives and acknowledges
socket.on("question", (data, callback) => {
  console.log("Server asked:", data);

  // Send response back
  callback({
    status: "ok",
    result: "processed",
  });
});

// Client requesting acknowledgement
socket.emit("action", "data", (response) => {
  console.log("Server responded:", response);
});

Connection handling

Connection lifecycle

// Server
io.on("connection", (socket) => {
  console.log("Connected:", socket.id);

  socket.on("disconnect", (reason) => {
    console.log("Disconnected:", reason);
    // Reasons: "server namespace disconnect",
    // "client namespace disconnect", "ping timeout", etc.
  });

  socket.on("disconnecting", () => {
    // Socket is about to disconnect
    console.log("Rooms:", socket.rooms);
  });
});

Manual disconnect

// Server disconnects client
socket.disconnect(); // Graceful disconnect
socket.disconnect(true); // Force disconnect

// Client disconnects
socket.disconnect();

// Client reconnects
socket.connect();

// Disable auto-reconnection
const socket = io({
  autoConnect: false,
});
socket.connect(); // Manual connect

Middleware

Server middleware

// Namespace middleware
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  if (isValidToken(token)) {
    socket.userId = getUserId(token);
    next();
  } else {
    next(new Error("Authentication error"));
  }
});

// Per-event middleware (v4.6+)
io.on("connection", (socket) => {
  socket.use(([event, ...args], next) => {
    console.log(`Event: ${event}`);
    next();
  });
});

// Error handling
io.use((socket, next) => {
  try {
    validateSocket(socket);
    next();
  } catch (error) {
    next(error);
  }
});

Client authentication

// Auth data in handshake
const socket = io({
  auth: {
    token: "jwt-token-here",
  },
});

// Access in middleware
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  // Validate token
  next();
});

// Dynamic auth
socket.auth = { token: "new-token" };
socket.connect();

Connection options

Server options

const io = new Server(server, {
  cors: {
    origin: "https://example.com",
    methods: ["GET", "POST"],
  },

  // Connection timeout
  connectTimeout: 45000,

  // Ping interval/timeout
  pingInterval: 25000,
  pingTimeout: 20000,

  // Max payload size
  maxHttpBufferSize: 1e6,

  // Allow upgrades to WebSocket
  allowUpgrades: true,

  // Transports allowed
  transports: ["websocket", "polling"],

  // Serve client files
  serveClient: false,
});

Client options

const socket = io("http://localhost:3000", {
  // Transports order
  transports: ["websocket", "polling"],

  // Auto-connect
  autoConnect: true,

  // Reconnection
  reconnection: true,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 5000,
  reconnectionAttempts: 5,

  // Query parameters
  query: {
    userId: "123",
  },

  // Auth
  auth: {
    token: "jwt-token",
  },

  // Upgrade timeout
  timeout: 20000,
});

Common patterns

Chat application

// Server
io.on("connection", (socket) => {
  // User joins room
  socket.on("join", (room, username) => {
    socket.join(room);
    socket.username = username;

    // Notify room
    socket.to(room).emit("userJoined", username);

    // Send room history
    socket.emit("history", getChatHistory(room));
  });

  // Receive message
  socket.on("message", (room, message) => {
    const msg = {
      user: socket.username,
      text: message,
      timestamp: Date.now(),
    };

    // Broadcast to room
    io.to(room).emit("message", msg);

    // Save to database
    saveChatMessage(room, msg);
  });

  // Typing indicator
  socket.on("typing", (room) => {
    socket.to(room).emit("userTyping", socket.username);
  });
});

// Client
socket.emit("join", "general", "John");

socket.on("message", (msg) => {
  displayMessage(msg);
});

document.getElementById("input").addEventListener("input", () => {
  socket.emit("typing", currentRoom);
});

Real-time notifications

// Server: Send notification to user
function notifyUser(userId, notification) {
  const userSockets = getUserSockets(userId);
  userSockets.forEach((socketId) => {
    io.to(socketId).emit("notification", notification);
  });
}

// Track user sockets
io.on("connection", (socket) => {
  socket.on("authenticate", (userId) => {
    socket.userId = userId;
    addUserSocket(userId, socket.id);

    // Send unread notifications
    socket.emit("notifications", getUnread(userId));
  });

  socket.on("disconnect", () => {
    removeUserSocket(socket.userId, socket.id);
  });
});

// Client: Display notifications
socket.on("notification", (notif) => {
  showNotification(notif.title, notif.message);
  updateBadgeCount();
});

Error handling

Server error handling

io.on("connection", (socket) => {
  // Handle client errors
  socket.on("error", (error) => {
    console.error("Socket error:", error);
  });

  // Catch event handler errors
  socket.on("riskyEvent", async (data) => {
    try {
      await processData(data);
      socket.emit("success");
    } catch (error) {
      socket.emit("error", {
        message: "Processing failed",
        code: error.code,
      });
    }
  });
});

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

// Client receives error
socket.on("connect_error", (error) => {
  console.error("Connection failed:", error.message);
});

Client error handling

// Connection errors
socket.on("connect_error", (error) => {
  if (error.message === "Authentication required") {
    // Re-authenticate
    socket.auth.token = getNewToken();
    socket.connect();
  }
});

// Timeout errors (v4.6+)
socket.timeout(5000).emit("data", data, (err, response) => {
  if (err) {
    console.error("Request timeout");
  }
});

// Custom error events
socket.on("error", (error) => {
  showErrorMessage(error.message);
});

// Reconnection events
socket.on("reconnect", (attemptNumber) => {
  console.log("Reconnected after", attemptNumber, "attempts");
});

socket.on("reconnect_error", (error) => {
  console.error("Reconnection failed");
});

socket.on("reconnect_failed", () => {
  console.error("All reconnection attempts failed");
});

Testing

Server-side testing

// Using socket.io-client for testing
const io = require("socket.io-client");
const ioServer = require("socket.io");

describe("Socket.io server", () => {
  let server, serverSocket, clientSocket;

  beforeAll((done) => {
    server = ioServer(3000);
    server.on("connection", (socket) => {
      serverSocket = socket;
    });

    clientSocket = io("http://localhost:3000");
    clientSocket.on("connect", done);
  });

  afterAll(() => {
    server.close();
    clientSocket.disconnect();
  });

  test("should emit message", (done) => {
    clientSocket.on("response", (data) => {
      expect(data).toBe("received");
      done();
    });

    serverSocket.emit("response", "received");
  });
});

Mock client testing

// Create test client
function createTestSocket() {
  const socket = io("http://localhost:3000", {
    autoConnect: false,
    auth: { token: "test-token" },
  });
  return socket;
}

// Test event emission
test("client sends message", (done) => {
  const socket = createTestSocket();

  socket.on("connect", () => {
    socket.emit("message", "test");

    socket.on("ack", (response) => {
      expect(response).toBe("ok");
      socket.disconnect();
      done();
    });
  });

  socket.connect();
});

TypeScript support

Server with types

import { Server, Socket } from "socket.io";

interface ServerToClientEvents {
  message: (data: string) => void;
  userJoined: (username: string) => void;
}

interface ClientToServerEvents {
  join: (room: string, username: string) => void;
  message: (room: string, text: string) => void;
}

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

interface SocketData {
  username: string;
  userId: string;
}

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

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

  // Events are type-checked
  socket.on("message", (room, text) => {
    // room: string, text: string
    io.to(room).emit("message", text);
  });
});

Client with types

import { io, Socket } from "socket.io-client";

interface ServerToClientEvents {
  message: (data: string) => void;
  userJoined: (username: string) => void;
}

interface ClientToServerEvents {
  join: (room: string, username: string) => void;
  message: (room: string, text: string) => void;
}

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

// Type-safe event handlers
socket.on("message", (data) => {
  // data is typed as string
  console.log(data);
});

// Type-safe emission
socket.emit("join", "general", "John");

Performance optimization

Volatile events

// Server: Skip if client not ready
socket.volatile.emit("tick", Date.now());

// Client: High-frequency updates
setInterval(() => {
  socket.volatile.emit("position", {
    x: player.x,
    y: player.y,
  });
}, 16); // 60fps, ok if some lost

Binary data

// Send buffer
const buffer = Buffer.from("binary data");
socket.emit("file", buffer);

// Receive buffer
socket.on("file", (buffer) => {
  // buffer is Buffer type
  console.log(buffer.length);
});

// Compression
socket.compress(true).emit("large", bigData);

Connection pooling

// Limit connections per IP
const connections = new Map();

io.use((socket, next) => {
  const ip = socket.handshake.address;
  const count = connections.get(ip) || 0;

  if (count >= 5) {
    return next(new Error("Too many connections"));
  }

  connections.set(ip, count + 1);

  socket.on("disconnect", () => {
    connections.set(ip, connections.get(ip) - 1);
  });

  next();
});

Redis adapter (Scaling)

Setup Redis adapter

// Install: npm install @socket.io/redis-adapter redis
import { Server } from "socket.io";
import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "redis";

const io = new Server(server);

// Create Redis clients
const pubClient = createClient({ url: "redis://localhost:6379" });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);

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

// Now works across multiple servers
io.emit("event", "data"); // All servers receive

Redis adapter features

// Emit to all servers
io.emit("globalEvent", "data");

// Server-local only
io.local.emit("localEvent", "data");

// Get socket count across servers
const sockets = await io.allSockets();
console.log("Total connected:", sockets.size);

// Get sockets in room (all servers)
const roomSockets = await io.in("room1").allSockets();

// Utility methods
const count = await io.serverSideEmit("countRequest");

Gotchas

⚠️ Socket ID changes

// Socket ID changes on reconnection
socket.on("connect", () => {
  console.log(socket.id); // Different each time!
});

// Use custom IDs for persistence
socket.on("authenticate", (userId) => {
  socket.userId = userId; // Store custom ID
});

// Track across reconnections
const userSockets = new Map(); // userId -> Set of socket IDs

⚠️ Memory leaks with listeners

// BAD: Listener never removed
socket.on("data", handler);

// GOOD: Remove listener
socket.on("disconnect", () => {
  socket.removeListener("data", handler);
});

// GOOD: Use once for one-time events
socket.once("init", (data) => {
  // Auto-removed after first call
});

// Remove all listeners
socket.removeAllListeners();

⚠️ Rooms auto-cleanup

// Rooms are auto-removed when empty
socket.join("room1");
socket.disconnect();
// "room1" deleted if it was the only socket

// Check room exists before emitting
const sockets = await io.in("room1").fetchSockets();
if (sockets.length > 0) {
  io.to("room1").emit("message", "data");
}

⚠️ Volatile vs regular events

// Regular: Guaranteed delivery (buffered if disconnected)
socket.emit("important", "data");

// Volatile: May be lost if client busy/disconnected
socket.volatile.emit("position", { x: 10, y: 20 });

// Use volatile for high-frequency, non-critical updates
// Use regular for important state changes

⚠️ Namespace separation

// Namespaces are completely separate
const chat = io.of("/chat");
const admin = io.of("/admin");

// This only emits to default namespace "/"
io.emit("event", "data");

// Emit to specific namespace
chat.emit("event", "data");

// Cross-namespace not supported
// Each client needs separate connections

⚠️ Acknowledgement order

// Callbacks execute BEFORE next event
socket.emit("first", "data", (response) => {
  console.log("1. Ack received");
});
socket.emit("second", "data");
// Server may receive "second" before responding to "first"

// Use timeouts for critical flows
socket.timeout(5000).emit("critical", data, (err, res) => {
  if (!err) {
    socket.emit("next", data);
  }
});

Version-specific features

v4.6+ features

// Timeout API
socket.timeout(5000).emit("request", data, (err, response) => {
  if (err) {
    console.error("Timeout");
  }
});

// Socket#except() - exclude specific sockets
socket.broadcast.except(socketId).emit("event", "data");

// Server-side events
io.serverSideEmit("customEvent", "data");

io.on("customEvent", (data) => {
  // Received on all servers
});

v4.0+ features

// Catch-all listeners
socket.onAny((eventName, ...args) => {
  console.log(`Event: ${eventName}`, args);
});

socket.onAnyOutgoing((eventName, ...args) => {
  console.log(`Sent: ${eventName}`, args);
});

// Disconnect other sockets
await socket.disconnectSockets();
await socket.in("room1").disconnectSockets();

// Fetch socket instances
const sockets = await io.fetchSockets();
const roomSockets = await io.in("room1").fetchSockets();

v3 to v4 migration

// v3: Broadcast to all
io.emit("event", "data");

// v4: Still works, but use explicit broadcast
io.emit("event", "data"); // Same behavior
socket.broadcast.emit("event", "data"); // Clearer intent

// v3: Rooms as array
socket.join(["room1", "room2"]);

// v4: Still works
socket.join("room1");
socket.join(["room1", "room2"]);

// v3: No TypeScript support
// v4: Full TypeScript support with generics

Also see