Skip to main content
Technology & EngineeringWebsocket248 lines

Socket Io

Socket.IO patterns for event-driven real-time communication with automatic reconnection and room management

Quick Summary18 lines
You are an expert in Socket.IO for building real-time applications with reliable, event-driven communication.

## Key Points

- **Define a typed event contract** using TypeScript interfaces for both client and server to catch mismatches at compile time.
- **Use rooms for group communication** instead of manually iterating over sockets. Rooms are optimized for broadcast.
- **Apply middleware for auth** rather than checking credentials inside each event handler.
- **Use acknowledgements for critical operations** where the sender needs confirmation (e.g., saving data, joining a room).
- **Set `maxHttpBufferSize`** to limit payload size and prevent abuse.
- **Prefer `socket.to(room)` over `io.in(room)`** when you want to exclude the sender from the broadcast.
- **Not handling `connect_error`** — without this handler, auth failures and network issues are silently swallowed on the client.
- **Assuming WebSocket transport** — Socket.IO may fall back to polling. Do not depend on a raw WebSocket connection existing.
- **Storing state on socket objects** — socket instances are ephemeral. Use `socket.data` for per-connection metadata and an external store for persistent state.
- **Ignoring the `disconnect` reason** — reasons like `"transport close"`, `"ping timeout"`, and `"server disconnect"` indicate very different problems.
- **Not handling `connect_error` on the client** — without this handler, authentication failures and network issues are silently swallowed; the client appears to work but never connects.
- **Confusing `io.emit`, `socket.emit`, `socket.to`, and `socket.broadcast`** — each has different targeting semantics; using the wrong one sends messages to too many or too few clients.
skilldb get websocket-skills/Socket IoFull skill: 248 lines
Paste into your CLAUDE.md or agent config

Socket.IO — WebSockets & Real-Time

You are an expert in Socket.IO for building real-time applications with reliable, event-driven communication.

Overview

Socket.IO is a library that enables low-latency, bidirectional, event-based communication between a client and server. It builds on top of WebSockets but adds automatic reconnection, packet buffering, multiplexing via namespaces, room-based broadcasting, and a fallback to HTTP long-polling when WebSockets are unavailable.

Core Concepts

Transport Layer

Socket.IO uses the Engine.IO protocol under the hood. It starts with HTTP long-polling for reliability, then upgrades to WebSocket when possible. This two-phase approach ensures connectivity even through restrictive proxies and firewalls.

Namespaces

Namespaces are communication channels that split the logic of your application over a single shared connection. The default namespace is /. Custom namespaces let you separate concerns (e.g., /chat, /notifications).

Rooms

Rooms are arbitrary channels that sockets can join and leave. They exist only on the server side and are the primary mechanism for targeted broadcasting.

Events

Socket.IO uses a custom event system on top of the WebSocket message channel. Both client and server can emit named events with structured payloads and optional acknowledgement callbacks.

Implementation Patterns

Server Setup

import { Server } from 'socket.io';
import { createServer } from 'http';
import express from 'express';

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
  cors: {
    origin: 'https://example.com',
    methods: ['GET', 'POST'],
  },
  pingInterval: 25000,
  pingTimeout: 20000,
  maxHttpBufferSize: 1e6, // 1 MB
});

// Authentication middleware
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    const user = verifyJWT(token);
    socket.data.user = user;
    next();
  } catch (err) {
    next(new Error('Authentication failed'));
  }
});

io.on('connection', (socket) => {
  console.log(`User connected: ${socket.data.user.id}`);

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

httpServer.listen(3000);

Client Setup

import { io } from 'socket.io-client';

const socket = io('https://example.com', {
  auth: { token: 'jwt-token-here' },
  reconnection: true,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 5000,
  reconnectionAttempts: 10,
});

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

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

Rooms and Broadcasting

// Server: room management
io.on('connection', (socket) => {
  // Join a room
  socket.on('join-room', (roomId) => {
    socket.join(roomId);
    socket.to(roomId).emit('user-joined', {
      userId: socket.data.user.id,
      name: socket.data.user.name,
    });
  });

  // Leave a room
  socket.on('leave-room', (roomId) => {
    socket.leave(roomId);
    socket.to(roomId).emit('user-left', { userId: socket.data.user.id });
  });

  // Broadcast to a room (excluding sender)
  socket.on('room-message', ({ roomId, content }) => {
    socket.to(roomId).emit('room-message', {
      from: socket.data.user.id,
      content,
      timestamp: Date.now(),
    });
  });

  // Broadcast to a room (including sender)
  socket.on('room-event', ({ roomId, event }) => {
    io.in(roomId).emit('room-event', event);
  });
});

Acknowledgements

// Server
socket.on('save-draft', (data, callback) => {
  try {
    const result = saveDraft(data);
    callback({ status: 'ok', id: result.id });
  } catch (err) {
    callback({ status: 'error', message: err.message });
  }
});

// Client
socket.emit('save-draft', { title: 'My doc' }, (response) => {
  if (response.status === 'ok') {
    console.log('Saved with id:', response.id);
  } else {
    console.error('Save failed:', response.message);
  }
});

// Client with timeout
socket.timeout(5000).emit('save-draft', { title: 'My doc' }, (err, response) => {
  if (err) {
    console.error('Server did not acknowledge in time');
  }
});

Namespaces

// Server: separate namespace for admin
const adminNamespace = io.of('/admin');

adminNamespace.use((socket, next) => {
  if (socket.data.user?.role !== 'admin') {
    return next(new Error('Unauthorized'));
  }
  next();
});

adminNamespace.on('connection', (socket) => {
  socket.on('get-stats', async (callback) => {
    const stats = await getServerStats();
    callback(stats);
  });
});

// Client
const adminSocket = io('https://example.com/admin', {
  auth: { token: adminToken },
});

Typed Events (TypeScript)

interface ServerToClientEvents {
  'room-message': (msg: { from: string; content: string; timestamp: number }) => void;
  'user-joined': (data: { userId: string; name: string }) => void;
}

interface ClientToServerEvents {
  'join-room': (roomId: string) => void;
  'room-message': (data: { roomId: string; content: string }) => void;
  'save-draft': (data: { title: string }, cb: (res: { status: string; id?: string }) => void) => void;
}

interface SocketData {
  user: { id: string; name: string; role: string };
}

const io = new Server<ClientToServerEvents, ServerToClientEvents, {}, SocketData>(httpServer);

Best Practices

  • Define a typed event contract using TypeScript interfaces for both client and server to catch mismatches at compile time.
  • Use rooms for group communication instead of manually iterating over sockets. Rooms are optimized for broadcast.
  • Apply middleware for auth rather than checking credentials inside each event handler.
  • Use acknowledgements for critical operations where the sender needs confirmation (e.g., saving data, joining a room).
  • Set maxHttpBufferSize to limit payload size and prevent abuse.
  • Prefer socket.to(room) over io.in(room) when you want to exclude the sender from the broadcast.

Common Pitfalls

  • Not handling connect_error — without this handler, auth failures and network issues are silently swallowed on the client.
  • Confusing io.emit, socket.emit, socket.to, and socket.broadcast — each has different targeting semantics. io.emit sends to all connected clients; socket.broadcast.emit sends to all except the sender.
  • Memory leaks from undisposed listeners — if you add listeners inside the connection handler on the server, they are scoped to that socket. But on the client, calling socket.on repeatedly (e.g., in a React useEffect without cleanup) leaks listeners.
  • Assuming WebSocket transport — Socket.IO may fall back to polling. Do not depend on a raw WebSocket connection existing.
  • Storing state on socket objects — socket instances are ephemeral. Use socket.data for per-connection metadata and an external store for persistent state.
  • Ignoring the disconnect reason — reasons like "transport close", "ping timeout", and "server disconnect" indicate very different problems.

Core Philosophy

Socket.IO is not raw WebSockets — it is a protocol and framework built on top of WebSockets with reliability features that raw WebSockets lack. Automatic reconnection, packet buffering during disconnection, acknowledgements with timeouts, and room-based broadcasting are features you would otherwise implement yourself, poorly. Use Socket.IO when you need these reliability features, and use raw WebSockets when you need maximum control and minimal overhead.

Rooms are the primary abstraction for group communication. They are more efficient and less error-prone than manually iterating over socket lists. When you need to send a message to a subset of clients, put those clients in a room and broadcast to it. When you need to exclude the sender, use socket.to(room). When you need to include the sender, use io.in(room). This distinction is subtle but important — confusing them is a common source of bugs.

Type safety prevents the most common Socket.IO bugs: misspelled event names, wrong payload shapes, and forgotten callback types. Define TypeScript interfaces for ServerToClientEvents, ClientToServerEvents, and SocketData, and pass them as generics to the Server and Socket types. This catches mismatches at compile time rather than at runtime in production.

Anti-Patterns

  • Not handling connect_error on the client — without this handler, authentication failures and network issues are silently swallowed; the client appears to work but never connects.

  • Confusing io.emit, socket.emit, socket.to, and socket.broadcast — each has different targeting semantics; using the wrong one sends messages to too many or too few clients.

  • Adding event listeners without cleanup in React — calling socket.on inside a useEffect without returning a cleanup function that calls socket.off leaks listeners on every re-render.

  • Storing application state on socket objects — socket instances are ephemeral and disappear when the connection drops; use socket.data for per-connection metadata and an external store for persistent state.

  • Assuming WebSocket transport is always used — Socket.IO may fall back to HTTP long-polling through restrictive proxies or firewalls; building features that depend on raw WebSocket behavior breaks in these environments.

Install this skill directly: skilldb add websocket-skills

Get CLI access →