Jesse A. Green / notebook

Socket.IO — Administrator & Developer 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. Architecture Overview

Socket.IO is a real-time, bidirectional, event-based communication library for the web. It provides a high-level abstraction over raw WebSocket connections, adding automatic reconnection, room-based broadcasting, namespace multiplexing, binary streaming, and connection state recovery. The library is designed so that developers never have to think about transport mechanics — Socket.IO negotiates the best available transport automatically and upgrades transparently from HTTP long-polling to WebSocket mid-session.

The architecture is deliberately layered. At the bottom, Engine.IO handles raw transport negotiation: it opens an HTTP long-polling connection first (maximizing compatibility with proxies, firewalls, and corporate networks), then probes for a WebSocket upgrade and switches seamlessly if available. On top of Engine.IO, Socket.IO adds the application-level protocol: namespaces for multiplexing multiple logical channels over a single connection, rooms for targeted broadcasting, middleware pipelines for authentication, acknowledgement callbacks, and binary event serialization. This separation means Engine.IO can be used standalone for minimal-overhead transport, while Socket.IO provides the full-featured developer experience.

The project ships as a TypeScript monorepo with 12 npm workspace packages. The server and client are separate packages, each with their own Engine.IO dependency. Adapters and emitters are pluggable — the default in-memory adapter works for single-server deployments, while cluster adapters and external emitters (Postgres, Redis Streams) enable horizontal scaling across multiple server instances.

1.1 Component Inventory

Package Role
socket.io Server-side library. Core classes: Server, Namespace, Socket, BroadcastOperator, RemoteSocket, Client, ParentNamespace. Attaches to Node.js HTTP servers or uWebSockets.js.
socket.io-client Client library for browsers and Node.js. Core classes: Manager (connection pooling), Socket (per-namespace), io() lookup function.
engine.io Low-level server transport layer. Classes: BaseServer, Server (Node.js http), uServer (uWebSockets.js), Socket, Transport (Polling, WebSocket, WebTransport).
engine.io-client Low-level client transport layer. Classes: SocketWithoutUpgrade, SocketWithUpgrade, Socket. Handles handshake, upgrade probing, heartbeat.
engine.io-parser Binary-safe packet encoding/decoding for the Engine.IO protocol. Encodes packets to strings or Buffers.
socket.io-parser Packet encoding/decoding for the Socket.IO protocol. Classes: Encoder, Decoder, BinaryReconstructor. Handles binary placeholder extraction and reconstruction.
socket.io-adapter In-memory room adapter and SessionAwareAdapter base class. Manages room membership, broadcasting, and connection state recovery sessions.
socket.io-cluster-adapter Cluster adapter with heartbeat-based node discovery. NodeClusterAdapter extends the base adapter for multi-process (Node.js cluster) or multi-server deployments.
socket.io-cluster-engine Cluster engine for distributing Engine.IO connections. NodeClusterEngine for cluster module, RedisEngine for Redis pub/sub.
socket.io-postgres-emitter Emitter that publishes events through PostgreSQL NOTIFY/LISTEN. Allows non-Socket.IO processes to broadcast to connected clients.
socket.io-redis-streams-emitter Emitter that publishes events through Redis Streams. Same use case as the Postgres emitter but using Redis as the transport.
@socket.io/component-emitter Typed event emitter base class. Provides on, off, once, emit, listeners, hasListeners with TypeScript generics for event maps.

Note

The monorepo uses npm workspaces. All 12 packages are developed, tested, and versioned together. The socket.io and socket.io-client packages are the primary entry points — the others are dependencies or optional scaling components.

1.2 Architecture Diagram

┌─────────────────────────────────────────────────────────────────────┐
│                        APPLICATION LAYER                            │
│                                                                     │
│   io.on("connection", socket => {                                   │
│     socket.join("room1");                                           │
│     socket.on("chat", msg => io.to("room1").emit("chat", msg));     │
│   });                                                               │
└──────────────────────────────┬──────────────────────────────────────┘
                               │
┌──────────────────────────────▼──────────────────────────────────────┐
│                     SOCKET.IO PROTOCOL LAYER                        │
│                                                                     │
│  ┌──────────┐  ┌───────────┐  ┌────────────────┐  ┌─────────────┐  │
│  │  Server   │  │ Namespace │  │BroadcastOperator│  │   Adapter   │  │
│  │          │──▶│  "/" or   │──▶│  .to() .emit() │──▶│  in-memory  │  │
│  │          │  │  "/chat"  │  │  immutable chain│  │  or cluster │  │
│  └──────────┘  └───────────┘  └────────────────┘  └─────────────┘  │
│       │                                                │            │
│  ┌────▼─────┐                                    ┌─────▼────────┐  │
│  │  Client  │         ┌──────────────┐           │  Emitters    │  │
│  │ (server) │         │ socket.io-   │           │  (Postgres,  │  │
│  └──────────┘         │ parser       │           │  Redis)      │  │
│                       │ Encoder/     │           └──────────────┘  │
│                       │ Decoder      │                              │
│                       └──────────────┘                              │
└──────────────────────────────┬──────────────────────────────────────┘
                               │
┌──────────────────────────────▼──────────────────────────────────────┐
│                      ENGINE.IO TRANSPORT LAYER                      │
│                                                                     │
│  ┌──────────────┐     ┌────────────┐     ┌───────────────────────┐  │
│  │  BaseServer   │     │   Socket   │     │     Transports        │  │
│  │  Server (http)│────▶│  per-conn  │────▶│  Polling (HTTP)       │  │
│  │  uServer (uWS)│     │  heartbeat │     │  WebSocket (ws)       │  │
│  └──────────────┘     └────────────┘     │  WebTransport (exp.)  │  │
│                                          └───────────────────────┘  │
│  ┌──────────────┐                                                   │
│  │ engine.io-   │  Packet encode/decode (string + binary)           │
│  │ parser       │                                                   │
│  └──────────────┘                                                   │
└──────────────────────────────┬──────────────────────────────────────┘
                               │
┌──────────────────────────────▼──────────────────────────────────────┐
│                         NETWORK LAYER                               │
│                                                                     │
│  Client ←──HTTP long-poll──→ Server    (initial, always works)      │
│  Client ←────WebSocket─────→ Server    (upgraded, low latency)      │
│  Client ←───WebTransport───→ Server    (experimental, HTTP/3)       │
└─────────────────────────────────────────────────────────────────────┘

1.3 Data Flow

  1. Client connects: The client calls io("https://server"). The Manager checks if a connection to this origin already exists (multiplexing). If not, it creates a new Engine.IO connection.

  2. Engine.IO handshake: The client sends an HTTP GET to {path}/ (default /socket.io/). The server responds with a JSON handshake containing the session ID (sid), available upgrades, and heartbeat parameters (pingInterval, pingTimeout).

  3. HTTP long-polling established: The client immediately begins polling. Messages flow over sequential HTTP requests. This guarantees connectivity even through aggressive proxies.

  4. Transport upgrade probe: If allowUpgrades is true (default) and the server advertised "websocket" in the handshake, the client opens a WebSocket connection and sends a probe packet. The server responds with a probe acknowledgement. The client then sends an upgrade packet, and both sides switch to the WebSocket transport. The polling transport is discarded.

  5. Socket.IO namespace negotiation: Once the Engine.IO connection is established, the client sends a Socket.IO CONNECT packet for each namespace it wants to join (starting with /). The server runs namespace middleware, and if accepted, responds with a CONNECT packet containing the socket ID.

  6. Event routing: Application events are encoded as Socket.IO EVENT packets, serialized through the parser (with binary placeholder extraction for non-string data), encoded through Engine.IO, and sent over the active transport. On the server side, incoming events are routed through the namespace to the appropriate socket's event handlers.

  7. Broadcasting via adapter: When the server broadcasts (io.to("room").emit("event", data)), the BroadcastOperator delegates to the Adapter, which iterates room membership sets and writes the encoded packet to each matching socket. In cluster mode, the adapter additionally publishes to other nodes via the cluster transport (Node.js IPC, Redis pub/sub, Postgres NOTIFY).

  8. Heartbeat: Engine.IO maintains liveness with ping/pong packets. The server sends ping, the client must respond with pong within pingTimeout milliseconds. Failure triggers a disconnect.

  9. Disconnect and reconnection: On disconnect, the server cleans up room memberships and emits the disconnect event. If connection state recovery is enabled, the server retains session data for maxDisconnectionDuration milliseconds. The client automatically reconnects (exponential backoff with jitter) and can recover its session, rooms, and missed events.

1.4 Technology Stack

Component Technology
Language TypeScript (compiled to JavaScript)
Runtime Node.js
WebSocket server ws (default), configurable via wsEngine
HTTP parsing Node.js http/https module, or uWebSockets.js
Testing Mocha, expect.js, supertest
Package management npm workspaces (monorepo)
Build TypeScript compiler (tsc)
CI GitHub Actions, Sauce Labs (cross-browser)

2. System Requirements

2.1 Runtime Requirements

Requirement Detail
Node.js v14+ (minimum for modern Socket.IO 4.x). Check individual package engines fields for exact constraints.
npm v7+ (required for workspace support in the monorepo)
Operating System Any platform supported by Node.js (Linux, macOS, Windows)
Network HTTP/HTTPS port accessible to clients. WebSocket upgrade must not be blocked by intermediary proxies (falls back to polling if blocked).

2.2 Installation

# Server
npm install socket.io

# Client (browser or Node.js)
npm install socket.io-client

# Optional: scaling adapters
npm install @socket.io/cluster-adapter
npm install @socket.io/postgres-emitter
npm install @socket.io/redis-streams-emitter

2.3 Browser Compatibility

The client library supports all modern browsers. Legacy browsers (IE 11) are no longer a target in Socket.IO 4.x. The client bundle is distributed as UMD and ESM formats.

2.4 Scaling Infrastructure

For multi-server deployments, you need one of:

Strategy Requires
Cluster adapter (Node.js cluster) No external dependency — uses Node.js IPC
Cluster adapter (Redis engine) Redis server
Postgres emitter PostgreSQL server
Redis Streams emitter Redis server with Streams support (5.0+)
Sticky sessions Load balancer with session affinity (required when using HTTP polling)

3. Configuration Reference

This section documents every configuration option available across the Socket.IO stack. All defaults are taken directly from source code.

3.1 Server Options (Socket.IO)

These options are passed to new Server(httpServer, options) or new Server(port, options). They are defined in ServerOptions within packages/socket.io/lib/index.ts.

Option Type Default Description
path string "/socket.io" The path under which Socket.IO is served. The client must connect using the same path. Trailing slash handling is controlled by addTrailingSlash.
serveClient boolean true Whether to serve the client JavaScript bundle at {path}/socket.io.js. Disable in production if you bundle the client yourself.
adapter AdapterConstructor Adapter (in-memory) The adapter class to use for room management and cross-server broadcasting. Pass the class, not an instance.
parser Parser socket.io-parser Custom parser for encoding/decoding Socket.IO packets. Must implement Encoder and Decoder interfaces.
connectTimeout number 45000 Milliseconds before disconnecting a client that has connected at the Engine.IO level but has not joined any namespace. Prevents resource exhaustion from half-open connections.
connectionStateRecovery object undefined Enable connection state recovery. When set, the server retains session data, room memberships, and missed events for disconnected clients.
connectionStateRecovery.maxDisconnectionDuration number 120000 Maximum time in milliseconds a session is retained after disconnection. Clients reconnecting after this window start fresh.
connectionStateRecovery.skipMiddlewares boolean true Whether to skip namespace middleware when a client successfully recovers its session. See Security section for implications.
cleanupEmptyChildNamespaces boolean false Automatically remove dynamically-created child namespaces when all clients disconnect. Useful for dynamic namespace patterns to prevent memory leaks.

Important

The connectionStateRecovery.skipMiddlewares option defaults to true, which means authentication middleware is bypassed when a client recovers a session. If your middleware performs authorization checks that should be enforced on every reconnection, set this to false. See the Security section for a detailed discussion.

3.2 Engine.IO Server Options

These options are also passed to the Socket.IO Server constructor and are forwarded to the underlying Engine.IO server. Defined in packages/engine.io/lib/server.ts.

Option Type Default Description
pingTimeout number 20000 How many milliseconds the server waits for a pong response before considering the connection dead.
pingInterval number 25000 How many milliseconds between each ping packet sent by the server. The total heartbeat cycle is pingInterval + pingTimeout.
upgradeTimeout number 10000 Maximum time in milliseconds for the transport upgrade probe to complete. If the WebSocket probe does not succeed within this window, the connection stays on polling.
maxHttpBufferSize number 1000000 (1 MB) Maximum allowed message size in bytes. Messages exceeding this are rejected and the connection is closed. Critical DoS protection.
transports string[] ["polling", "websocket"] Allowed transports. Set to ["websocket"] to disable polling (eliminates need for sticky sessions but sacrifices fallback compatibility).
allowUpgrades boolean true Whether to allow transport upgrades (polling to WebSocket). Set to false to force clients to stay on their initial transport.
perMessageDeflate boolean | object false Enable per-message deflate compression for WebSocket frames. Reduces bandwidth at the cost of CPU. See Performance section for tuning.
httpCompression boolean | object true Enable gzip/deflate compression for HTTP polling responses. Default threshold: 1024 bytes (responses smaller than this are not compressed).
wsEngine any ws.Server WebSocket server implementation. Override to use a different WebSocket library.
cookie CookieSerializeOptions | boolean false Set an HTTP cookie on the handshake response. Used for sticky-session affinity with load balancers that support cookie-based routing.
cors CorsOptions undefined CORS configuration object passed directly to the cors npm package. Required when the client is served from a different origin than the server.
allowEIO3 boolean false Accept connections from Engine.IO v3 clients (Socket.IO v2.x). Enable during migration from v2 to v4.
allowRequest function undefined A function (req, callback) called on every new connection attempt. Call callback(null, true) to accept, callback(reason, false) to reject. Runs before any Socket.IO middleware.
initialPacket any undefined A packet sent to the client immediately upon connection, before any application data.

3.3 Engine.IO Attach Options

These options control how Engine.IO attaches to an HTTP server. Defined in AttachOptions in packages/engine.io/lib/server.ts.

Option Type Default Description
path string "/engine.io" URL path for Engine.IO requests. When used through Socket.IO, this is overridden by the Socket.IO path option.
destroyUpgrade boolean true Whether to destroy unhandled HTTP upgrade requests. When true, WebSocket upgrade requests that don't match Engine.IO are terminated.
destroyUpgradeTimeout number 1000 Milliseconds to wait before destroying unhandled upgrade requests.
addTrailingSlash boolean true Whether the path should match with or without a trailing slash. When true, both /socket.io and /socket.io/ are handled.

3.4 Client Manager Options

These options are passed to io(url, options) or new Manager(url, options). Defined in packages/socket.io-client/lib/manager.ts.

Option Type Default Description
forceNew boolean false Force a new Manager instance instead of reusing an existing one for the same URL. Alias: multiplex: false.
multiplex boolean true Reuse an existing Manager for the same URL. Multiple namespace connections share one Engine.IO connection.
path string "/socket.io" Must match the server's path option.
reconnection boolean true Enable automatic reconnection after disconnect.
reconnectionAttempts number Infinity Maximum number of reconnection attempts before giving up.
reconnectionDelay number 1000 Initial delay in milliseconds before the first reconnection attempt.
reconnectionDelayMax number 5000 Maximum delay between reconnection attempts. The actual delay increases exponentially from reconnectionDelay up to this value.
randomizationFactor number 0.5 Jitter factor applied to reconnection delay. A value of 0.5 means the actual delay varies between 50% and 150% of the computed delay. Prevents thundering herd on server restart.
timeout number 20000 Connection timeout in milliseconds. If the connection is not established within this time, a connect_error event is emitted.
autoConnect boolean true Automatically connect on Manager creation. Set to false to manually call manager.open().
parser Parser socket.io-parser Custom parser. Must match the server's parser.

3.5 Client Socket Options

Additional options for individual namespace sockets (passed via io(url, options)):

Option Type Default Description
auth object | function undefined Authentication payload sent with the CONNECT packet. Can be a static object or a function (cb) => cb(payload) for dynamic tokens.
retries number undefined Number of retries for event acknowledgements before giving up.
ackTimeout number undefined Timeout in milliseconds for acknowledgement callbacks.

3.6 Engine.IO Client Socket Options

Low-level transport options set on the client side (forwarded through Manager):

Option Type Default Description
upgrade boolean true Whether to attempt transport upgrade.
rememberUpgrade boolean false Remember if a previous connection to this host succeeded with WebSocket and skip the polling phase on reconnection.
transportOptions object {} Per-transport options (e.g., different headers for polling vs WebSocket).
closeOnBeforeunload boolean false Whether to close the transport on the beforeunload event (browser).
withCredentials boolean false Whether to include credentials (cookies) in cross-origin requests.

3.7 HTTP Compression Options

When httpCompression is an object:

Option Type Default Description
threshold number 1024 Minimum response size in bytes before compression is applied.

Standard zlib options (level, memLevel, strategy, etc.) are also accepted and forwarded to Node.js zlib.createDeflate() / zlib.createGzip().

3.8 Per-Message Deflate Options

When perMessageDeflate is an object:

Option Type Default Description
threshold number 1024 Minimum message size before compression is applied.

Standard ws per-message-deflate options (zlibDeflateOptions, zlibInflateOptions, serverNoContextTakeover, etc.) are also accepted.

3.9 Adapter Configuration

In-Memory Adapter (default)

No configuration needed. Suitable for single-server deployments only.

Cluster Adapter

import { createAdapter } from "@socket.io/cluster-adapter";

io.adapter(createAdapter());

The cluster adapter uses Node.js cluster module IPC by default. For Redis-backed clustering:

import { createAdapter } from "@socket.io/cluster-adapter";
import { RedisEngine } from "@socket.io/cluster-engine";
import { createClient } from "redis";

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

io.adapter(createAdapter(new RedisEngine(pubClient, subClient)));

Session-Aware Adapter

The SessionAwareAdapter extends the base adapter to support connection state recovery. It maintains a session store (Map<PrivateSessionId, Session>) and a packet buffer for missed-event replay.


4. Deployment & Scaling

4.1 Single Server Deployment

The simplest deployment: one Node.js process, one Socket.IO server, the default in-memory adapter.

import { createServer } from "http";
import { Server } from "socket.io";

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

io.on("connection", (socket) => {
  // handle events
});

httpServer.listen(3000);

This handles thousands of concurrent connections on a single modern server. Limits depend on available memory (each connection consumes roughly 10-20 KB), file descriptors (ulimit -n), and event processing throughput.

4.2 Multi-Server with Node.js Cluster

Use the cluster adapter to scale across CPU cores on a single machine:

import cluster from "cluster";
import { createServer } from "http";
import { Server } from "socket.io";
import { setupMaster, setupWorker } from "@socket.io/sticky";
import { createAdapter, setupPrimary } from "@socket.io/cluster-adapter";

if (cluster.isPrimary) {
  const httpServer = createServer();
  setupMaster(httpServer, { loadBalancingMethod: "least-connection" });
  setupPrimary();
  httpServer.listen(3000);

  for (let i = 0; i < require("os").cpus().length; i++) {
    cluster.fork();
  }
} else {
  const httpServer = createServer();
  const io = new Server(httpServer);
  io.adapter(createAdapter());
  setupWorker(io);
}

Tip

The @socket.io/sticky module provides sticky session routing at the Node.js level, eliminating the need for an external load balancer when using cluster. It routes based on the sid query parameter or the io cookie.

4.3 Multi-Server with External Adapters

For deployments across multiple machines, use an adapter backed by an external message broker:

Redis Streams Emitter (publish from non-Socket.IO processes):

import { Emitter } from "@socket.io/redis-streams-emitter";
import { createClient } from "redis";

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

const emitter = new Emitter(client);
emitter.to("room1").emit("hello", "world");

Postgres Emitter (publish via PostgreSQL):

import { Emitter } from "@socket.io/postgres-emitter";
import pg from "pg";

const pool = new pg.Pool({ connectionString: "postgres://localhost/mydb" });
const emitter = new Emitter(pool);
emitter.to("room1").emit("hello", "world");

4.4 uWebSockets.js Integration

For maximum throughput, Socket.IO supports uWebSockets.js as an alternative to Node.js http:

import { App } from "uWebSockets.js";
import { Server } from "socket.io";

const app = App();
const io = new Server();

io.attachApp(app);

io.on("connection", (socket) => {
  // handle events
});

app.listen(3000, (token) => {
  if (!token) {
    console.error("Failed to listen");
  }
});

The uServer class in Engine.IO handles the uWebSockets.js-specific API differences (non-standard request/response objects, different upgrade handling).

Note

When using uWebSockets.js, the initial_headers and headers events on io.engine may not fire in all cases. This is a known limitation (see issue #5300). Workaround: use uWebSockets.js native header APIs directly.

4.5 Sticky Sessions

When HTTP long-polling is enabled (the default), clients send multiple HTTP requests during a single session. All requests for a given session must reach the same server process. This requires sticky sessions at the load balancer level.

Strategies for sticky routing:

Method How it works
Cookie-based Set cookie: { name: "io", path: "/", httpOnly: true, sameSite: "strict" } on the server. Configure your load balancer to route based on this cookie.
IP hash Route based on client IP. Simpler but breaks with NAT/proxy.
sid parameter Route based on the sid query parameter. Requires load balancer regex support.
WebSocket-only Set transports: ["websocket"] on both server and client. No sticky sessions needed — WebSocket is a single persistent connection.

Nginx configuration for sticky sessions:

upstream socket_io {
    ip_hash;  # or use sticky cookie
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    server 127.0.0.1:3003;
}

server {
    listen 80;
    server_name your-domain.com;

    location /socket.io/ {
        proxy_pass http://socket_io;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

HAProxy configuration:

frontend http
    bind *:80
    default_backend socket_io

backend socket_io
    balance source  # IP-based sticky
    # Or use cookie-based:
    # cookie io insert indirect nocache
    # server srv1 127.0.0.1:3001 cookie srv1
    server srv1 127.0.0.1:3001
    server srv2 127.0.0.1:3002
    server srv3 127.0.0.1:3003

    # Required for WebSocket
    option http-server-close
    option forceclose

4.6 Docker Deployment

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Key Docker considerations:

  • Health checks: Use the Engine.IO health endpoint or implement a custom one (see Operations section).
  • Graceful shutdown: Handle SIGTERM to call io.close() before container termination (see Operations section).
  • Network mode: In bridge mode, ensure the container port is properly mapped and WebSocket upgrades pass through the Docker proxy.
  • Resource limits: Set memory limits based on expected connection count (roughly 20 KB per connection baseline).
  • Scaling with Docker Compose: When running multiple replicas behind a reverse proxy, ensure sticky sessions are configured and all instances share an adapter backend (Redis, Postgres).

Docker Compose with Redis adapter:

version: "3.8"
services:
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  socketio:
    build: .
    deploy:
      replicas: 3
    environment:
      - REDIS_URL=redis://redis:6379
    depends_on:
      - redis

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - socketio

4.7 CloudFlare Compatibility

CloudFlare terminates WebSocket connections after 100 seconds of inactivity by default. The default Socket.IO heartbeat interval (25 seconds) keeps connections alive well within this window. However, aggressive CloudFlare plans or custom rules may interfere.

Relevant issue: #2580 — CloudFlare compatibility has been a recurring topic. Ensure:

  • WebSocket support is enabled in your CloudFlare plan.
  • The pingInterval is shorter than CloudFlare's idle timeout.
  • If using polling fallback, CloudFlare's HTTP timeout (100 seconds) must accommodate long-poll requests.

4.8 AWS Application Load Balancer

When deploying behind AWS ALB:

  • Enable sticky sessions (target group setting) using application-based cookies.
  • Set the idle timeout to at least pingInterval + pingTimeout + 5000 ms.
  • WebSocket connections are supported natively by ALB. No special configuration needed beyond ensuring the security group allows the port.

5. Operations

5.1 Connection Monitoring

// Total connected Engine.IO clients
console.log(io.engine.clientsCount);

// Connected sockets in a namespace
const sockets = await io.in("room1").fetchSockets();
console.log(`${sockets.length} sockets in room1`);

// All rooms
const rooms = io.of("/").adapter.rooms;
console.log(`${rooms.size} rooms`);

// All socket IDs
const sids = io.of("/").adapter.sids;
console.log(`${sids.size} connected sockets`);

Tip

For cluster deployments, fetchSockets() queries all nodes via the adapter. Use io.local.fetchSockets() (the local flag) to query only the current server, avoiding cross-node overhead in monitoring tight loops.

Periodic metrics collection:

setInterval(() => {
  const metrics = {
    timestamp: new Date().toISOString(),
    connections: io.engine.clientsCount,
    rooms: io.of("/").adapter.rooms.size,
    memory: process.memoryUsage().heapUsed,
    uptime: process.uptime()
  };
  // Push to your monitoring system (Prometheus, Datadog, etc.)
  pushMetrics(metrics);
}, 10000);

Connection lifecycle events:

io.engine.on("connection", (rawSocket) => {
  console.log(`Engine.IO connection: ${rawSocket.id}, transport: ${rawSocket.transport.name}`);

  rawSocket.on("upgrade", (transport) => {
    console.log(`Transport upgraded to: ${transport.name}`);
  });

  rawSocket.on("close", (reason) => {
    console.log(`Engine.IO close: ${rawSocket.id}, reason: ${reason}`);
  });
});

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

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

Note

Disconnect reasons include: "server namespace disconnect", "client namespace disconnect", "server shutting down", "ping timeout", "transport close", "transport error", and "parse error". Logging these provides insight into connection health patterns.

5.2 Graceful Shutdown

Socket.IO servers must be shut down gracefully to avoid dropped connections and data loss:

process.on("SIGTERM", () => {
  // Stop accepting new connections
  io.close(() => {
    console.log("Socket.IO server closed");
    process.exit(0);
  });

  // Force exit after timeout
  setTimeout(() => {
    console.error("Forced shutdown after timeout");
    process.exit(1);
  }, 10000);
});

The io.close() method:

  1. Closes the underlying HTTP server (stops accepting new connections).
  2. Closes all existing Engine.IO connections (sends close packets).
  3. Cleans up adapter resources.

For zero-downtime deployments, drain connections gradually:

process.on("SIGTERM", async () => {
  // Notify clients to reconnect to another server
  io.emit("server:shutdown", { deadline: Date.now() + 30000 });

  // Stop accepting new connections at the Engine.IO level
  io.engine.close();

  // Wait for clients to reconnect to other instances
  await new Promise(resolve => setTimeout(resolve, 30000));

  process.exit(0);
});

5.3 Health Checking

Implement a health endpoint for load balancers and orchestrators:

import { createServer } from "http";

const httpServer = createServer((req, res) => {
  if (req.url === "/health") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({
      status: "ok",
      connections: io.engine.clientsCount,
      uptime: process.uptime(),
      memory: process.memoryUsage()
    }));
    return;
  }
  // Socket.IO handles other requests
});

const io = new Server(httpServer);
httpServer.listen(3000);

For Kubernetes liveness/readiness probes:

livenessProbe:
  httpGet:
    path: /health
    port: 3000
  initialDelaySeconds: 5
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /health
    port: 3000
  initialDelaySeconds: 5
  periodSeconds: 5

5.4 Logging

Socket.IO uses the debug module for all internal logging. Enable logging by setting the DEBUG environment variable:

# All Socket.IO logs
DEBUG=socket.io:* node server.js

# All Engine.IO logs
DEBUG=engine:* node server.js

# Both
DEBUG=socket.io:*,engine:* node server.js

# Specific components
DEBUG=socket.io:server node server.js
DEBUG=socket.io:socket node server.js
DEBUG=socket.io:namespace node server.js
DEBUG=engine:transport node server.js
DEBUG=engine:socket node server.js

# Client-side (browser)
localStorage.debug = "socket.io-client:*";

# Client-side (Node.js)
DEBUG=socket.io-client:* node client.js

Production logging strategy:

In production, avoid enabling DEBUG=* (it is extremely verbose). Instead:

  1. Use application-level logging for business events.
  2. Enable DEBUG=socket.io:server,engine:server for connection lifecycle only.
  3. Enable full debug only during active troubleshooting.
  4. Redirect debug output to a file or log aggregator rather than stdout.

5.5 Connection State Recovery

Connection state recovery allows clients to resume their session after a brief disconnection, recovering room memberships and missed events without re-authenticating.

Setup:

const io = new Server(httpServer, {
  connectionStateRecovery: {
    maxDisconnectionDuration: 120000,  // 2 minutes
    skipMiddlewares: false             // re-run auth on recovery
  }
});

Monitoring recovery:

io.on("connection", (socket) => {
  if (socket.recovered) {
    console.log(`Socket ${socket.id} recovered session`);
    // No need to re-send state -- the client has it
  } else {
    console.log(`Socket ${socket.id} is a new connection`);
    // Send initial state
  }
});

The SessionAwareAdapter maintains:

  • A sessions Map keyed by private session ID, containing room memberships and the last known offset.
  • A packet buffer (array of [offset, packet, broadcastOptions] tuples) for replaying missed events.

Sessions are automatically cleaned up when maxDisconnectionDuration expires.

Memory implications: Each retained session stores room membership and buffered packets. For high-traffic applications, the packet buffer can grow significantly. Monitor memory usage when enabling connection state recovery, especially with large maxDisconnectionDuration values.

5.6 Adapter Management

Monitoring Adapter Health

For cluster adapters, monitor node discovery:

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}`);
});

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

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

Room Inspection

// List all application rooms (excluding personal rooms)
for (const [room, sids] of io.of("/").adapter.rooms) {
  if (!io.of("/").adapter.sids.has(room)) {
    // Exclude personal rooms (each socket has a room matching its ID)
    console.log(`Room ${room}: ${sids.size} members`);
  }
}

// Check if a specific room exists and its size
const roomMembers = io.of("/").adapter.rooms.get("room1");
if (roomMembers) {
  console.log(`room1 has ${roomMembers.size} members`);
}

Programmatic Room Management

// Force all sockets in a room to join another room
await io.in("room1").socketsJoin("room2");

// Force all sockets in a room to leave
await io.in("room1").socketsLeave("room1");

// Disconnect all sockets in a room
await io.in("room1").disconnectSockets(true);  // true = close underlying connection

6. Performance

6.1 Transport Selection

The transport choice has the largest impact on latency and throughput:

Transport Latency Throughput Compatibility
HTTP long-polling High (round-trip per message batch) Low Universal (works through any proxy)
WebSocket Low (sub-millisecond framing overhead) High Requires proxy/firewall support
WebTransport Lowest (QUIC/HTTP3) Highest Experimental, limited browser support

WebSocket-only mode eliminates the polling overhead entirely and removes the sticky session requirement:

// Server
const io = new Server(httpServer, {
  transports: ["websocket"]
});

// Client
const socket = io("https://server", {
  transports: ["websocket"]
});

Caution

WebSocket-only mode sacrifices the polling fallback. Clients behind restrictive corporate proxies or firewalls that block WebSocket upgrades will fail to connect entirely. Use this only in controlled environments where you can guarantee WebSocket support.

6.2 Per-Message Deflate

The perMessageDeflate option compresses individual WebSocket frames using the permessage-deflate extension:

const io = new Server(httpServer, {
  perMessageDeflate: {
    threshold: 2048,  // only compress messages > 2KB
    zlibDeflateOptions: {
      level: 1  // fastest compression (1-9, lower = faster)
    }
  }
});

Tradeoffs:

Factor Impact
Bandwidth Reduced 60-80% for text-heavy messages
CPU Significant increase (deflate per frame)
Memory Increased (deflate context per connection, ~300 KB each)
Latency Slight increase (compression time)

Default is false (disabled). Enable only when bandwidth is the bottleneck and CPU is abundant. The memory overhead per connection is substantial — with 10,000 connections, the deflate contexts alone consume roughly 3 GB.

6.3 HTTP Compression

HTTP compression for polling responses is enabled by default with a 1024-byte threshold:

const io = new Server(httpServer, {
  httpCompression: {
    threshold: 2048  // increase threshold for lower CPU usage
  }
});

Disable entirely for maximum throughput when bandwidth is not a concern:

const io = new Server(httpServer, {
  httpCompression: false
});

6.4 Volatile Messages

The volatile flag drops messages if the client is not ready to receive them (transport is not writable). Useful for high-frequency data like cursor positions or game state where only the latest value matters:

socket.volatile.emit("cursor", { x: 100, y: 200 });

// Also works with broadcasting
io.volatile.to("game-room").emit("state", gameState);

This avoids buffering during transport upgrades or temporary disconnections. Messages sent with volatile are fire-and-forget — there is no way to know if the client received them.

6.5 Local Broadcasting

The local flag restricts broadcasting to the current server only, avoiding the overhead of cross-node communication via the adapter:

// Only broadcast to clients connected to THIS server
io.local.emit("local-event", data);

// Combine with room targeting
io.local.to("room1").emit("local-event", data);

Useful for server-specific events (like resource usage metrics) or when you know all target clients are on the current node.

6.6 maxHttpBufferSize Tuning

The default maxHttpBufferSize of 1 MB protects against memory exhaustion from oversized messages. Adjust based on your application's needs:

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

// Chat application (text only)
const io = new Server(httpServer, {
  maxHttpBufferSize: 64e3  // 64 KB
});

Setting this too high exposes the server to memory-based DoS attacks. Setting it too low causes legitimate large messages (file uploads, large JSON payloads) to be silently dropped with a connection close. The client receives a transport close disconnect reason with no indication that the message was too large.

6.7 Binary Data Handling

Socket.IO automatically detects binary data (Buffer, ArrayBuffer, Blob) in emitted events and uses the binary serialization path. The parser extracts binary attachments, replaces them with placeholder objects ({ _placeholder: true, num: 0 }), encodes the JSON skeleton, and sends the binary attachments as separate frames.

For applications that send mostly binary data, consider:

  • Using Engine.IO directly to avoid the Socket.IO protocol overhead (namespace framing, packet type headers).
  • Using a custom parser (e.g., msgpack) that handles binary data natively without placeholder extraction.
  • Keeping maxHttpBufferSize appropriately sized for your binary payloads.

6.8 Connection Pooling via Manager

The client's Manager implements connection multiplexing: multiple namespace connections to the same server share a single Engine.IO transport connection. This is enabled by default (multiplex: true).

// These two share one Engine.IO connection
const chat = io("/chat");
const notifications = io("/notifications");

// This creates a separate Engine.IO connection
const separate = io("/separate", { forceNew: true });

Multiplexing reduces handshake overhead and file descriptor usage on both client and server. Disable with forceNew: true only when you need independent reconnection behavior or different transport options per namespace.

6.9 Connection Count Optimization

Each Engine.IO connection consumes:

  • One file descriptor (WebSocket) or periodic HTTP requests (polling).
  • Memory for the socket state, buffers, and room memberships.
  • CPU for heartbeat timers and packet serialization.

Strategies for scaling to 100K+ connections per server:

Strategy Savings
WebSocket-only transport Eliminates HTTP overhead, fewer file descriptors
Disable perMessageDeflate Save ~300 KB memory per connection
Reduce pingInterval Lower timer overhead (but increases disconnect detection latency)
Increase pingTimeout Tolerate slow clients, reduce false disconnections
Use volatile for non-critical events Reduce buffer memory
Minimize room memberships Reduce adapter iteration time

6.10 Benchmarking

To benchmark your specific deployment:

// Server-side: track message throughput
let messageCount = 0;
io.on("connection", (socket) => {
  socket.onAny(() => { messageCount++; });
});

setInterval(() => {
  console.log(`Messages/sec: ${messageCount}`);
  messageCount = 0;
}, 1000);

Typical throughput on modern hardware (single process, WebSocket transport):

  • Simple echo: 50,000-100,000 messages/second
  • With room broadcasting (100 rooms, 100 clients each): 10,000-30,000 broadcasts/second
  • With per-message deflate: 5,000-15,000 messages/second (CPU-bound)

7. Security

7.1 CORS Configuration

Cross-Origin Resource Sharing must be configured when the client and server are on different origins. Socket.IO passes the cors option directly to the cors npm package:

const io = new Server(httpServer, {
  cors: {
    origin: ["https://your-app.com", "https://admin.your-app.com"],
    methods: ["GET", "POST"],
    credentials: true
  }
});
Setting Recommendation
origin Always specify explicit origins. Never use "*" in production with credentials: true.
methods ["GET", "POST"] is sufficient for Socket.IO.
credentials Set to true if using cookie-based authentication or sticky sessions.

Warning

Setting cors.origin to "*" disables CORS protection entirely. Any website can connect to your Socket.IO server and interact with your application. Combined with credentials: true, this allows cross-site request forgery attacks. Always whitelist specific origins in production.

7.2 Connection Validation with allowRequest

The allowRequest callback runs at the Engine.IO level, before any Socket.IO middleware. It receives the raw HTTP request and can reject connections based on headers, IP, or any request attribute:

const io = new Server(httpServer, {
  allowRequest: (req, callback) => {
    const origin = req.headers.origin;
    const allowed = ["https://your-app.com"];

    if (allowed.includes(origin)) {
      callback(null, true);
    } else {
      callback("FORBIDDEN", false);
    }
  }
});

This is the lowest-level rejection point — the connection is refused before Engine.IO allocates resources for it. Use it for:

  • IP-based rate limiting
  • Geoblocking
  • Origin validation as a defense-in-depth layer
  • Token validation when you want to reject before any resource allocation

7.3 Middleware-Based Authentication

Socket.IO namespaces support middleware pipelines via io.use(). Middleware runs during the CONNECT phase (after Engine.IO handshake, before the socket joins the namespace):

io.use((socket, next) => {
  const token = socket.handshake.auth.token;

  try {
    const user = jwt.verify(token, SECRET);
    socket.data.user = user;
    next();
  } catch (err) {
    next(new Error("Authentication failed"));
  }
});

The client sends authentication data via the auth option:

const socket = io("https://server", {
  auth: {
    token: "eyJhbGciOiJI..."
  }
});

// Or dynamically (re-evaluated on each reconnection)
const socket = io("https://server", {
  auth: (cb) => {
    cb({ token: getLatestToken() });
  }
});

Multiple middleware functions run in registration order. Any middleware calling next(error) aborts the connection and sends a CONNECT_ERROR packet to the client.

Per-namespace middleware:

const adminNs = io.of("/admin");
adminNs.use((socket, next) => {
  if (socket.data.user?.role === "admin") {
    next();
  } else {
    next(new Error("Forbidden"));
  }
});

7.4 connectionStateRecovery and Authentication Bypass

Caution

When connectionStateRecovery is enabled with the default skipMiddlewares: true, clients that successfully recover a session bypass all namespace middleware, including authentication checks. This means:

  • A revoked token continues to work for maxDisconnectionDuration milliseconds after revocation.
  • Role/permission changes are not enforced until the session expires.
  • If the session private ID is compromised, an attacker can hijack the session without authentication.

Mitigation: Set skipMiddlewares: false to re-run middleware on every recovery. This adds latency to reconnection but ensures authorization is always checked. See issue #5491 for community discussion.

7.5 DoS Prevention

Attack Vector Protection Default
Large messages maxHttpBufferSize 1 MB
Connection flooding connectTimeout 45 seconds
Slowloris pingTimeout 20 seconds
Half-open connections upgradeTimeout 10 seconds
Resource exhaustion allowRequest callback None (must implement)

For additional protection:

const io = new Server(httpServer, {
  maxHttpBufferSize: 1e6,
  connectTimeout: 10000,
  allowRequest: (req, cb) => {
    const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress;
    if (isRateLimited(ip)) {
      cb("RATE_LIMITED", false);
    } else {
      cb(null, true);
    }
  }
});

Application-level rate limiting:

io.use((socket, next) => {
  const ip = socket.handshake.address;
  const key = `rate:${ip}`;

  if (rateLimiter.isExceeded(key)) {
    next(new Error("Rate limit exceeded"));
  } else {
    rateLimiter.increment(key);
    next();
  }
});

// Per-event rate limiting
socket.on("message", (data) => {
  if (socket.data.messageCount > 100) {
    socket.disconnect(true);
    return;
  }
  socket.data.messageCount = (socket.data.messageCount || 0) + 1;
  // process message
});

7.6 Namespace Isolation

Namespaces provide logical separation of concerns. Each namespace has its own:

  • Connection event handlers
  • Middleware pipeline
  • Room set
  • Authentication logic
const adminNs = io.of("/admin");
adminNs.use(requireAdminRole);
adminNs.on("connection", (socket) => {
  // Only admin users reach here
});

const publicNs = io.of("/public");
publicNs.on("connection", (socket) => {
  // Any authenticated user
});

Namespaces are not a security boundary by themselves — they share the same Engine.IO connection. An authenticated client can attempt to connect to any namespace. The middleware pipeline on each namespace is the enforcement point.

When using cookie-based sticky sessions:

const io = new Server(httpServer, {
  cookie: {
    name: "io",
    path: "/",
    httpOnly: true,
    sameSite: "strict",
    secure: true  // HTTPS only
  }
});

Always set httpOnly, secure, and sameSite in production to prevent cookie theft via XSS and CSRF attacks.

7.8 Input Validation

Socket.IO does not perform any validation on event data. All input from clients must be validated in event handlers:

socket.on("createRoom", (name) => {
  // ALWAYS validate input
  if (typeof name !== "string" || name.length > 50 || !/^[a-zA-Z0-9-]+$/.test(name)) {
    socket.emit("error", "Invalid room name");
    return;
  }

  socket.join(name);
});

Binary data should be validated for size and format. JSON payloads should be validated with a schema validator (Zod, Joi, etc.).

7.9 Transport Security

  • Always use HTTPS/WSS in production. Socket.IO inherits the TLS configuration from the underlying HTTP server.
  • For mTLS (mutual TLS), note that issue #5480 reports compatibility issues with PKCS11 hardware security modules. File-based certificates work reliably.
  • The withCredentials option on the client must be true for cookies to be sent cross-origin.

8. Developer Internals

8.1 Repository Structure

The repository is a TypeScript monorepo with 12 packages in packages/:

socket.io/
├── packages/
│   ├── socket.io/                       # Server library
│   │   └── lib/
│   │       ├── index.ts                 # Server class
│   │       ├── namespace.ts             # Namespace class
│   │       ├── socket.ts               # Server-side Socket class
│   │       ├── broadcast-operator.ts    # BroadcastOperator
│   │       ├── client.ts               # Client (per-connection)
│   │       └── parent-namespace.ts      # Dynamic namespace support
│   ├── socket.io-client/                # Client library
│   │   └── lib/
│   │       ├── index.ts                 # io() lookup function
│   │       ├── manager.ts              # Manager class
│   │       ├── socket.ts               # Client-side Socket class
│   │       └── url.ts                  # URL parsing
│   ├── engine.io/                       # Engine.IO server
│   │   └── lib/
│   │       ├── server.ts               # BaseServer, Server
│   │       ├── userver.ts              # uServer (uWebSockets.js)
│   │       ├── socket.ts               # Engine.IO Socket
│   │       └── transports/             # Polling, WebSocket, WebTransport
│   ├── engine.io-client/                # Engine.IO client
│   │   └── lib/
│   │       ├── socket.ts               # Socket classes
│   │       └── transports/             # Client transports
│   ├── engine.io-parser/                # Engine.IO protocol codec
│   ├── socket.io-parser/                # Socket.IO protocol codec
│   ├── socket.io-adapter/              # Base adapter
│   ├── socket.io-cluster-adapter/       # Cluster adapter
│   ├── socket.io-cluster-engine/        # Cluster engine
│   ├── socket.io-postgres-emitter/      # Postgres emitter
│   ├── socket.io-redis-streams-emitter/ # Redis Streams emitter
│   └── socket.io-component-emitter/     # Typed event emitter
├── examples/                            # Example applications
└── test/                                # Integration tests

8.2 Class Hierarchy

The codebase contains 53 classes total. Here is the inheritance and delegation structure:

Server Layer

Server (packages/socket.io/lib/index.ts)
  ├── owns Map<string, Namespace>
  ├── creates Client for each Engine.IO connection
  └── delegates to Namespace

Namespace (namespace.ts)
  ├── owns Map<SocketId, Socket>
  ├── uses Adapter for room management
  ├── creates BroadcastOperator for broadcasting
  └── runs middleware pipeline

BroadcastOperator (broadcast-operator.ts)
  ├── immutable chaining: .to() / .except() / .in() return NEW instances
  ├── delegates to Adapter.broadcast()
  └── supports .emit(), .fetchSockets(), .socketsJoin(), .socketsLeave(), .disconnectSockets()

RemoteSocket
  └── read-only proxy for sockets on remote nodes (via adapter)

ParentNamespace (parent-namespace.ts)
  └── dynamic namespace factory (regex/function matching)

Client (client.ts)
  └── per-Engine.IO-connection, manages namespace socket map

Client Layer

io() function (packages/socket.io-client/lib/index.ts)
  └── lookup cache: reuse Manager for same URL (multiplexing)

Manager (manager.ts)
  ├── owns one Engine.IO Socket (transport connection)
  ├── manages reconnection logic (backoff, jitter)
  ├── creates Socket per namespace
  └── emits: open, close, error, reconnect, reconnect_attempt, reconnect_error, reconnect_failed

Socket (socket.ts)
  ├── per-namespace connection
  ├── sends/receives events through Manager's Engine.IO connection
  ├── handles auth, ack callbacks, binary events
  └── emits: connect, disconnect, connect_error

Engine.IO Layer

BaseServer (packages/engine.io/lib/server.ts)
  ├── abstract: handles handshake, session management
  ├── owns Map<string, Socket>
  └── abstract methods: createTransport(), init()

Server extends BaseServer
  └── Node.js http.Server integration

uServer extends BaseServer
  └── uWebSockets.js App integration

Socket (socket.ts)
  ├── per-connection state machine
  ├── manages transport upgrade lifecycle
  ├── runs heartbeat (ping/pong)
  └── emits: open, close, message, error, upgrading, upgrade

Transport (abstract)
  ├── Polling → handles HTTP request/response cycle
  ├── WebSocket → wraps ws connection
  └── WebTransport → wraps WebTransport session

Engine.IO Client Layer

SocketWithoutUpgrade (packages/engine.io-client/lib/socket.ts)
  └── base: single transport, no upgrade

SocketWithUpgrade extends SocketWithoutUpgrade
  └── adds transport upgrade probing

Socket extends SocketWithUpgrade
  └── full-featured client (default export)

Parser Layer

Encoder (packages/socket.io-parser/)
  └── encodes Packet → string + binary attachments

Decoder
  └── decodes string + binary attachments → Packet
  └── uses BinaryReconstructor for multi-part binary packets

BinaryReconstructor
  └── reassembles binary packets from placeholder skeleton + binary frames

Adapter Hierarchy

Adapter (packages/socket.io-adapter/)
  ├── in-memory room management (Map<Room, Set<SocketId>>)
  ├── broadcasting (iterate rooms, filter, write)
  └── emits: join-room, leave-room, create-room, delete-room

SessionAwareAdapter extends Adapter
  ├── adds session store (Map<PrivateSessionId, Session>)
  ├── packet buffer for missed-event replay
  └── connection state recovery logic

NodeClusterAdapter (packages/socket.io-cluster-adapter/)
  └── extends Adapter with cross-node communication

Emitters

Emitter (packages/socket.io-postgres-emitter/ or redis-streams-emitter/)
  ├── creates BroadcastOperator instances
  ├── publishes events to external store
  └── allows non-Socket.IO processes to broadcast to connected clients

BroadcastOperator (within each emitter package)
  └── immutable chaining, same API as server-side BroadcastOperator

8.3 Socket.IO Protocol

Socket.IO uses its own protocol layered on top of Engine.IO. Packet types defined in socket.io-parser:

Type Value Direction Purpose
CONNECT 0 Both Namespace join request (client to server) or acknowledgement (server to client)
DISCONNECT 1 Both Graceful namespace leave
EVENT 2 Both Application event with JSON data
ACK 3 Both Acknowledgement callback response
CONNECT_ERROR 4 Server to Client Namespace join rejected (middleware failure)
BINARY_EVENT 5 Both Application event containing binary data
BINARY_ACK 6 Both Acknowledgement containing binary data

Packet format (string encoding): {type}[{namespace},][{ack_id}][{json_data}]

Examples:

0/chat,                                    CONNECT to /chat namespace
2["hello","world"]                         EVENT on default namespace
2/chat,["msg","hi"]                        EVENT on /chat namespace
52-["binary",{"_placeholder":true,"num":0}]  BINARY_EVENT with 2 attachments
3/chat,7["received"]                       ACK on /chat, ack ID 7
4/chat,{"message":"Not authorized"}        CONNECT_ERROR on /chat

8.4 Engine.IO Protocol

Engine.IO protocol packet types:

Type Value Purpose
open 0 Server handshake (contains sid, upgrades, pingInterval, pingTimeout)
close 1 Request to close connection
ping 2 Heartbeat request (server to client)
pong 3 Heartbeat response (client to server)
message 4 Application data (Socket.IO packets are carried as message payloads)
upgrade 5 Transport upgrade confirmation
noop 6 No-op (used during upgrade probing)

Handshake response format:

{
  "sid": "lv_VI97HAXpY6yYWAAAC",
  "upgrades": ["websocket"],
  "pingInterval": 25000,
  "pingTimeout": 20000,
  "maxPayload": 1000000
}

8.5 Transport Upgrade Flow

Client                                  Server
  |                                       |
  |--GET /socket.io/?EIO=4&transport=     |
  |  polling-----------------------------→|
  |                                       |
  |←-------------- 200 OK ---------------|  (handshake: sid, upgrades)
  |                                       |
  |--GET /socket.io/?EIO=4&transport=     |
  |  polling&sid=xxx---------------------→|  (long-poll, waiting)
  |                                       |
  |--WS /socket.io/?EIO=4&transport=      |
  |  websocket&sid=xxx-------------------→|  (open WebSocket)
  |                                       |
  |--WS "2probe" ------------------------→|  (probe ping)
  |                                       |
  |←-WS "3probe" ------------------------|  (probe pong)
  |                                       |
  |--WS "5" -----------------------------→|  (upgrade)
  |                                       |
  |  (polling connection closed)           |
  |                                       |
  |<======= WebSocket only =============>|  (all traffic on WS)

The upgrade is seamless: no messages are lost. The server buffers messages during the upgrade transition and flushes them over the new transport.

8.6 BroadcastOperator Pattern

The BroadcastOperator class implements an immutable builder pattern. Every method that modifies the targeting (.to(), .except(), .in()) returns a new BroadcastOperator instance rather than mutating state:

// Each call returns a new BroadcastOperator
const op1 = io.to("room1");        // new BroadcastOperator targeting room1
const op2 = op1.except("room2");   // new BroadcastOperator targeting room1, excluding room2
const op3 = op2.volatile;          // new BroadcastOperator with volatile flag

// op1, op2, op3 are all independent -- no shared mutable state
op1.emit("event1");  // room1 only
op2.emit("event2");  // room1 except room2
op3.emit("event3");  // room1 except room2, volatile

This design prevents accidental state leakage between broadcast operations and is safe for concurrent use. The pattern is replicated in the emitter packages (Postgres, Redis Streams) so external processes get the same chaining API.

8.7 Dynamic Namespaces

The ParentNamespace class enables dynamic namespace creation via regex or function matching:

// Regex match
io.of(/^\/user-\w+$/);

// Function match
io.of((name, auth, next) => {
  const isValid = checkNamespace(name);
  next(null, isValid);
});

When a client connects to a namespace that matches the pattern, a new child Namespace is created on-the-fly. If cleanupEmptyChildNamespaces is true, empty child namespaces are automatically removed when all clients disconnect.

Limitations:

  • fetchSockets() is not supported on ParentNamespace instances. Calling it throws: "fetchSockets() is not supported on parent namespaces". Query individual child namespaces instead.
  • Each child namespace has its own middleware pipeline (inherited from the parent at creation time). Middleware added to the parent after a child is created does not apply retroactively.

8.8 Error Messages Reference

Parser errors (socket.io-parser):

Error Cause
"illegal attachments" Number of expected binary attachments is invalid (negative or non-numeric).
"too many attachments" Received more binary frames than expected by the packet header.
"unknown type" Packet type byte is not a recognized Socket.IO packet type (0-6).
"invalid payload" JSON payload in a packet could not be parsed.
"got plaintext data when reconstructing a packet" Received a string frame while expecting binary attachment frames.
"got binary data when not reconstructing a packet" Received a binary frame when no binary packet reconstruction is in progress.
"Unknown type: {type}" Encoder received a packet with an unknown type.

Emitter errors:

Error Cause
"Acknowledgements are not supported" emitWithAck() called on a Postgres or Redis Streams emitter. Emitters are fire-and-forget; they cannot receive acknowledgements because they are not Socket.IO servers.

ParentNamespace errors:

Error Cause
"fetchSockets() is not supported on parent namespaces" Cannot aggregate sockets across dynamically-created child namespaces. Query individual child namespaces instead.

Engine.IO disconnect reasons:

Reason Meaning
"transport close" Transport connection lost (network failure, proxy timeout).
"transport error" Transport encountered an error.
"server close" Server called socket.close() or io.close().
"ping timeout" Client did not respond to ping within pingTimeout.
"parse error" Received data could not be parsed.
"forced close" Server forcefully closed the connection.

Socket.IO disconnect reasons:

Reason Side Meaning
"server namespace disconnect" Server Server called socket.disconnect().
"client namespace disconnect" Client Client called socket.disconnect().
"server shutting down" Server Server is closing (io.close()).
"ping timeout" Engine Heartbeat failed.
"transport close" Engine Transport connection lost.
"transport error" Engine Transport error.
"parse error" Engine Invalid packet received.

8.9 TypeScript Patterns

The codebase makes extensive use of TypeScript generics for type-safe event handling:

// Server-side typed events
interface ServerToClientEvents {
  hello: (message: string) => void;
  notification: (data: { title: string; body: string }) => void;
}

interface ClientToServerEvents {
  chat: (message: string, callback: (ok: boolean) => void) => void;
  joinRoom: (room: string) => void;
}

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

interface SocketData {
  user: string;
  role: "admin" | "user";
}

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

io.on("connection", (socket) => {
  socket.data.user = "alice";          // typed: SocketData
  socket.emit("hello", "world");       // typed: ServerToClientEvents
  socket.on("chat", (msg, cb) => {     // typed: ClientToServerEvents
    cb(true);
  });
  // socket.emit("nonexistent");       // compile error
  // socket.on("nonexistent", () => {});// compile error
});

Key type utilities:

  • StrictEventEmitter — enforces event names and argument types at compile time. Unused event names cause compile errors.
  • TypedEventBroadcaster — applies type constraints to BroadcastOperator, ensuring io.emit() and socket.broadcast.emit() are type-checked.
  • DecorateAcknowledgements — transforms event map types to support emitWithAck() return types, deriving the acknowledgement return type from the callback parameter.
  • EventsMap — base constraint type for event maps (Record<string, (...args: any[]) => void>).

8.10 Testing Strategy

The repository includes integration tests across packages and example applications that serve as end-to-end tests:

Example Purpose
angular-todomvc Angular integration with real-time sync
basic-crud-application CRUD with Socket.IO change notifications
basic-websocket-client Raw WebSocket client connecting to Socket.IO server (protocol compatibility)
custom-parsers Custom parser implementations (msgpack)
nestjs-example NestJS framework integration
private-messaging Multi-room private messaging with sessions

Unit tests use Mocha and expect.js. Integration tests spin up actual HTTP servers, connect real clients, and verify end-to-end message delivery, reconnection, and broadcasting. Cross-browser testing is run via Sauce Labs in CI.

8.11 Key Design Decisions

Polling-first transport selection: Socket.IO starts with HTTP long-polling and upgrades to WebSocket, rather than starting with WebSocket and falling back. This ensures the connection is established as quickly as possible (no WebSocket handshake timeout delay) and maximizes compatibility.

Immutable BroadcastOperator: The builder pattern avoids shared mutable state, making broadcasting safe in async contexts where multiple handlers may build broadcast targets concurrently.

File descriptor per connection (not per namespace): Namespace multiplexing over a single Engine.IO connection is a deliberate choice to minimize resource usage. Alternative designs with separate connections per namespace would simplify the protocol but increase resource consumption by orders of magnitude.

Adapter abstraction: The adapter interface is minimal (rooms, broadcasting, socket listing) to keep the contract implementable by diverse backends (in-memory, Redis, Postgres, custom).

Potential improvements:

  • The transports option array is mutated by the constructor (issue #5462). This should be cloned internally.
  • The heartbeat timeout is not reset by non-ping packets (issue #5450), which can cause false disconnections on busy connections.
  • The emitWithAck memory leak (issue #4984) suggests that pending ack tracking should be tied to socket lifecycle.
  • Connection state recovery's default skipMiddlewares: true is a security concern that arguably should default to false.

9. Integration Points

9.1 HTTP Framework Integration

Express:

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

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer);

// Express routes and Socket.IO coexist on the same HTTP server
app.get("/", (req, res) => res.sendFile("index.html"));
io.on("connection", (socket) => { /* ... */ });

httpServer.listen(3000);

Koa:

import Koa from "koa";
import { createServer } from "http";
import { Server } from "socket.io";

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

httpServer.listen(3000);

Fastify:

import fastify from "fastify";
import { Server } from "socket.io";

const app = fastify();
await app.ready();

const io = new Server(app.server);

io.on("connection", (socket) => { /* ... */ });

app.listen({ port: 3000 });

NestJS:

Socket.IO integrates with NestJS via the @nestjs/websockets module and @nestjs/platform-socket.io adapter. The example in examples/nestjs-example/ demonstrates gateway-based event handling with decorators:

@WebSocketGateway()
export class EventsGateway {
  @SubscribeMessage("events")
  handleEvent(@MessageBody() data: string): string {
    return data;
  }
}

9.2 uWebSockets.js Integration

For maximum performance, bypass Node.js HTTP entirely:

import { App } from "uWebSockets.js";
import { Server } from "socket.io";

const app = App();
const io = new Server();
io.attachApp(app);

app.listen(3000, (token) => {
  if (token) console.log("Listening on port 3000");
});

The Engine.IO uServer class handles the differences between uWebSockets.js and Node.js HTTP APIs. Key differences:

  • HttpRequest and HttpResponse are not Node.js streams.
  • Upgrade handling is synchronous.
  • Response must be completed before the handler returns (or corked).
  • Some Engine.IO events (initial_headers, headers) may not fire (issue #5300).

9.3 Custom Adapters

Extend the base Adapter class to implement custom room management and cross-server broadcasting:

import { Adapter, BroadcastOptions } from "@socket.io/adapter";

class MyAdapter extends Adapter {
  async broadcast(packet: any, opts: BroadcastOptions): Promise<void> {
    // 1. Call super to handle local sockets
    super.broadcast(packet, opts);

    // 2. Publish to your message broker for other servers
    await this.publishToExternalBroker(packet, opts);
  }

  // Override other methods as needed:
  // addAll(id, rooms) — called when a socket joins rooms
  // del(id, room) — called when a socket leaves a room
  // delAll(id) — called when a socket disconnects
  // fetchSockets(opts) — query sockets across nodes
  // serverSideEmit(packet) — inter-server communication
}

The adapter interface is intentionally minimal. The required methods are:

  • addAll(id: SocketId, rooms: Set<Room>) — add a socket to rooms
  • del(id: SocketId, room: Room) — remove a socket from a room
  • delAll(id: SocketId) — remove a socket from all rooms
  • broadcast(packet, opts: BroadcastOptions) — broadcast a packet
  • fetchSockets(opts) — query connected sockets

9.4 Custom Parsers

Implement the Encoder and Decoder interfaces for alternative serialization formats:

import msgpack from "msgpack-lite";

class MsgPackEncoder {
  encode(packet) {
    return [msgpack.encode(packet)];
  }
}

class MsgPackDecoder extends Emitter {
  add(chunk) {
    const packet = msgpack.decode(chunk);
    this.emit("decoded", packet);
  }
  destroy() {}
}

// Server
const io = new Server(httpServer, {
  parser: { Encoder: MsgPackEncoder, Decoder: MsgPackDecoder }
});

// Client -- must use the same parser
const socket = io("https://server", {
  parser: { Encoder: MsgPackEncoder, Decoder: MsgPackDecoder }
});

Custom parsers must handle binary data correctly. The default parser uses placeholder-based binary extraction; a msgpack parser can embed binary data directly, which is more efficient for binary-heavy payloads.

9.5 Emitters for External Processes

Emitters allow non-Socket.IO processes (background workers, microservices, cron jobs) to broadcast to connected clients:

// In a background worker (no Socket.IO server needed)
import { Emitter } from "@socket.io/redis-streams-emitter";
import { createClient } from "redis";

const redis = createClient();
await redis.connect();

const emitter = new Emitter(redis);

// Same API as server-side broadcasting
emitter.to("room1").emit("notification", { text: "Job complete" });
emitter.of("/admin").emit("alert", { level: "warning" });

// Target specific sockets
emitter.to(socketId).emit("private", { data: "for you only" });

Postgres emitter with connection pooling:

import { Emitter } from "@socket.io/postgres-emitter";
import pg from "pg";

const pool = new pg.Pool({
  connectionString: "postgres://user:pass@localhost/mydb",
  max: 10
});

const emitter = new Emitter(pool);

// Broadcast from any process that can reach the database
emitter.to("updates").emit("data-changed", { table: "users", id: 42 });

Limitations of emitters:

  • Cannot receive events from clients (one-way, server-to-client only).
  • Acknowledgements are not supported (emitWithAck() throws "Acknowledgements are not supported").
  • Cannot query connected sockets (fetchSockets() not available).
  • Cannot disconnect sockets or manage rooms.

9.6 Accessing Socket.IO from Express Routes

Share the io instance with HTTP route handlers:

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer);

// Make io available in routes
app.set("io", io);

app.post("/api/notify", (req, res) => {
  const io = req.app.get("io");
  io.to("notifications").emit("new-notification", req.body);
  res.json({ ok: true });
});

10. Troubleshooting

10.1 Connection Issues

Connection fails silently

Symptoms: Client emits connect_error but no useful error message.

Diagnosis:

  1. Enable debug logging: DEBUG=socket.io:*,engine:* node server.js
  2. Check that the path option matches between client and server (default: /socket.io).
  3. Verify CORS configuration if client and server are on different origins.
  4. Check that the server port is accessible and not firewalled.
  5. If behind a reverse proxy, ensure it forwards the Upgrade and Connection headers.

Warning

Issue #5288: Firefox may throw CORS errors during reconnection when the Origin header changes between the initial connection and reconnection. Ensure your CORS configuration handles all valid origins consistently. The error manifests as connect_error with a generic CORS message.

Frequent disconnections

Symptoms: Clients disconnect and reconnect repeatedly.

Diagnosis:

  1. Check pingTimeout and pingInterval. If the server is under heavy load, it may not send pings on time. Increase pingTimeout.
  2. Check for timer reliability issues. Node.js timers are not guaranteed to fire exactly on time under load (issue #5357).
  3. Check for proxy timeouts. Many reverse proxies (Nginx, CloudFlare, AWS ALB) have idle connection timeouts shorter than the heartbeat interval.
  4. Check client-side: mobile browsers may suspend WebSocket connections when the app is backgrounded.

Tip

Issue #5450 reports that the heartbeat timeout is not reset when non-ping packets are received. This means a busy connection that sends many application messages but experiences a delayed ping can still time out. Workaround: reduce pingInterval to send pings more frequently, or increase pingTimeout to be more tolerant.

Client reconnect broken after upgrade

Issue #5197: Client version 4.8.0 had a regression where reconnection logic failed after a transport upgrade. The state machine did not properly reset on disconnect. Upgrade to the latest patch release.

Messages not delivered in multi-server setup

Symptoms: Events emitted on one server do not reach clients connected to other servers.

Diagnosis:

  1. Verify the adapter is correctly configured on all server instances.
  2. Check that the external message broker (Redis, Postgres) is accessible from all servers.
  3. Issue #5456: ClusterAdapter.broadcast() can drop events when publishAndReturnOffset throws. Ensure error handling in the adapter layer.
  4. Check sticky sessions: if polling transport is used, requests from the same client must reach the same server.

ESM import issues

Caution

Issue #5215: In some ESM bundler configurations, the internal _placeholder property used for binary reconstruction gets renamed (e.g., to it). This breaks binary event parsing completely -- binary events arrive as empty objects. Workaround: ensure your bundler does not mangle property names in Socket.IO internals (exclude from minification), or pin to a fixed version.

10.2 Performance Issues

Memory leaks with emitWithAck

Issue #4984: When using emitWithAck() with a timeout, the acknowledgement callback is not cleaned up if the client disconnects before responding. Over time, this causes memory growth proportional to the number of timed-out acknowledgements.

Workaround: Manually track pending acks and clean up on disconnect:

socket.on("disconnect", () => {
  // Clean up any pending ack timers
  clearPendingAcks(socket.id);
});

High memory usage with many connections

Check for:

  • perMessageDeflate: true — each connection allocates a deflate context (~300 KB). Disable if not needed.
  • Connection state recovery — retained sessions and packet buffers consume memory proportional to maxDisconnectionDuration and message volume.
  • Large event payloads buffered during transport upgrade.

High CPU usage

Check for:

  • perMessageDeflate: true — compression is CPU-intensive.
  • httpCompression: true with low threshold — compressing small responses wastes CPU.
  • Broadcasting to very large rooms — the adapter iterates all room members and encodes per socket.

10.3 Known Issues from GitHub

Issue Summary Status/Workaround
#5197 Reconnect broken in client 4.8.0 Upgrade to latest patch release. The reconnection logic had a regression in state management.
#4984 emitWithAck memory leak on timeout Track and clean up pending acks manually on disconnect.
#5480 mTLS error with PKCS11 Hardware security module (HSM) integration for TLS client certificates fails in certain configurations. Workaround: use file-based certificates.
#5357 Timer reliability issues causing false disconnections Node.js setTimeout is not precise under load. Increase pingTimeout or reduce server load.
#5491 connectionStateRecovery.skipMiddlewares bypasses auth Set skipMiddlewares: false if authorization must be re-checked on recovery.
#5288 Firefox CORS error during reconnection Firefox rejects CORS preflight on reconnection when credentials change. Ensure consistent CORS headers.
#5215 ESM _placeholder replaced with it by bundlers Configure bundler to preserve property names in Socket.IO packages.
#5456 ClusterAdapter.broadcast() drops events on error publishAndReturnOffset failures cause silent event loss. Add error handling/retry logic.
#5300 uWebSockets.js initial_headers event not emitted Use uWebSockets.js native header APIs instead of Engine.IO events.
#5450 Heartbeat timeout not reset on non-ping packets Reduce pingInterval to send pings more frequently.
#5462 transports options array modified by constructor Pass a new array each time, or clone before passing. The constructor mutates the input array.
#2580 CloudFlare compatibility Ensure WebSocket support is enabled in CloudFlare plan. Keep pingInterval below CloudFlare's idle timeout.

10.4 Diagnostic Checklist

[ ] Server and client Socket.IO versions are compatible
[ ] path option matches (default: /socket.io)
[ ] CORS is configured (if cross-origin)
[ ] Sticky sessions configured (if using polling with multiple servers)
[ ] Adapter is installed and configured (if multi-server)
[ ] Firewall allows WebSocket upgrades (HTTP 101)
[ ] Reverse proxy forwards Upgrade and Connection headers
[ ] pingTimeout is large enough for server load
[ ] maxHttpBufferSize matches application needs
[ ] DEBUG logging enabled for diagnosis
[ ] Node.js version is 14+
[ ] No bundler mangling Socket.IO internal properties
[ ] Cookie configuration is correct (if using cookie-based sticky sessions)

10.5 Debug Logging Examples

Server not receiving events:

# Enable server-side socket debug
DEBUG=socket.io:socket node server.js
# Look for "got packet" entries -- if absent, packets are not reaching the namespace

Transport upgrade failing:

# Enable Engine.IO transport debug
DEBUG=engine:transport,engine:socket node server.js
# Look for "probe" and "upgrade" entries
# Common failure: proxy strips Upgrade header

Adapter not broadcasting:

# Enable namespace and adapter debug
DEBUG=socket.io:namespace node server.js
# Verify room membership with:
# io.of("/").adapter.rooms

11. Quick Reference

11.1 Server API Summary

// Create server
const io = new Server(httpServer, options);
const io = new Server(port, options);

// Namespace
const ns = io.of("/namespace");
ns.use(middleware);
ns.on("connection", (socket) => { });

// Broadcasting
io.emit("event", data);                    // all clients, default namespace
io.to("room").emit("event", data);         // room members only
io.except("room").emit("event", data);     // everyone except room
io.to("r1").to("r2").emit("event", data);  // union of rooms
io.local.emit("event", data);              // current server only
io.volatile.emit("event", data);           // drop if not ready
io.of("/ns").emit("event", data);          // specific namespace

// Socket
socket.join("room");
socket.leave("room");
socket.to("room").emit("event", data);     // broadcast to room (excludes sender)
socket.rooms;                               // Set<string> of rooms
socket.id;                                  // socket ID
socket.handshake;                           // auth, headers, query, etc.
socket.data;                                // arbitrary data store
socket.disconnect(close?);                  // disconnect

// Acknowledgements
socket.emit("event", data, (response) => { });
const response = await socket.emitWithAck("event", data);

// Fetch remote sockets (works across cluster)
const sockets = await io.fetchSockets();
for (const s of sockets) {
  s.emit("event", data);
  s.join("room");
  s.leave("room");
  s.disconnect();
}

// Room management
await io.in("room1").socketsJoin("room2");
await io.in("room1").socketsLeave("room2");
await io.in("room1").disconnectSockets(true);

// Server lifecycle
io.close(callback);
io.engine.clientsCount;

11.2 Client API Summary

// Connect
const socket = io("https://server");
const socket = io("https://server/namespace", { auth: { token } });

// Events
socket.on("connect", () => { });
socket.on("disconnect", (reason) => { });
socket.on("connect_error", (err) => { });
socket.on("event", (data) => { });
socket.onAny((event, ...args) => { });

// Emit
socket.emit("event", data);
socket.emit("event", data, (ack) => { });
const ack = await socket.emitWithAck("event", data);

// Lifecycle
socket.connect();
socket.disconnect();
socket.connected;   // boolean
socket.recovered;   // boolean (connection state recovery)
socket.id;          // string
socket.io;          // Manager instance

11.3 Debug Namespaces

Namespace Shows
socket.io:server Server lifecycle, namespace creation
socket.io:socket Server-side socket events
socket.io:namespace Namespace operations
socket.io:client Per-connection client management
socket.io-client:manager Client Manager (connect, reconnect)
socket.io-client:socket Client Socket (events, acks)
engine:server Engine.IO server (handshake, upgrade)
engine:socket Engine.IO socket (transport, heartbeat)
engine:transport Transport operations (polling, websocket)
engine:client Engine.IO client

11.4 Common Configuration Recipes

High-performance WebSocket-only:

const io = new Server(httpServer, {
  transports: ["websocket"],
  perMessageDeflate: false,
  httpCompression: false,
  serveClient: false
});

Maximum compatibility:

const io = new Server(httpServer, {
  transports: ["polling", "websocket"],
  allowEIO3: true,
  cookie: { name: "io", httpOnly: true },
  pingTimeout: 30000
});

Secure production:

const io = new Server(httpServer, {
  cors: {
    origin: "https://your-app.com",
    credentials: true
  },
  maxHttpBufferSize: 1e6,
  connectTimeout: 10000,
  serveClient: false,
  connectionStateRecovery: {
    maxDisconnectionDuration: 60000,
    skipMiddlewares: false
  }
});

Scaled deployment with Redis:

import { createAdapter } from "@socket.io/cluster-adapter";
import { RedisEngine } from "@socket.io/cluster-engine";

const io = new Server(httpServer, {
  adapter: createAdapter(new RedisEngine(pubClient, subClient)),
  cookie: { name: "io", httpOnly: true, sameSite: "strict", secure: true }
});

Low-latency gaming:

const io = new Server(httpServer, {
  transports: ["websocket"],
  perMessageDeflate: false,
  httpCompression: false,
  pingInterval: 5000,
  pingTimeout: 10000,
  serveClient: false
});

io.on("connection", (socket) => {
  // Use volatile for position updates (latest state matters, not every frame)
  setInterval(() => {
    socket.volatile.emit("gameState", getCurrentState());
  }, 50); // 20 Hz
});