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.ioandsocket.io-clientpackages 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
-
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. -
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). -
HTTP long-polling established: The client immediately begins polling. Messages flow over sequential HTTP requests. This guarantees connectivity even through aggressive proxies.
-
Transport upgrade probe: If
allowUpgradesistrue(default) and the server advertised"websocket"in the handshake, the client opens a WebSocket connection and sends aprobepacket. The server responds with aprobeacknowledgement. The client then sends anupgradepacket, and both sides switch to the WebSocket transport. The polling transport is discarded. -
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. -
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.
-
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). -
Heartbeat: Engine.IO maintains liveness with ping/pong packets. The server sends
ping, the client must respond withpongwithinpingTimeoutmilliseconds. Failure triggers a disconnect. -
Disconnect and reconnection: On disconnect, the server cleans up room memberships and emits the
disconnectevent. If connection state recovery is enabled, the server retains session data formaxDisconnectionDurationmilliseconds. 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.skipMiddlewaresoption defaults totrue, 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 tofalse. 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/stickymodule provides sticky session routing at the Node.js level, eliminating the need for an external load balancer when usingcluster. It routes based on thesidquery parameter or theiocookie.
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_headersandheadersevents onio.enginemay 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
SIGTERMto callio.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
pingIntervalis 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 + 5000ms. - 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. Useio.local.fetchSockets()(thelocalflag) 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:
- Closes the underlying HTTP server (stops accepting new connections).
- Closes all existing Engine.IO connections (sends close packets).
- 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:
- Use application-level logging for business events.
- Enable
DEBUG=socket.io:server,engine:serverfor connection lifecycle only. - Enable full debug only during active troubleshooting.
- 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
sessionsMap 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
maxHttpBufferSizeappropriately 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.originto"*"disables CORS protection entirely. Any website can connect to your Socket.IO server and interact with your application. Combined withcredentials: 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
connectionStateRecoveryis enabled with the defaultskipMiddlewares: true, clients that successfully recover a session bypass all namespace middleware, including authentication checks. This means:
- A revoked token continues to work for
maxDisconnectionDurationmilliseconds 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: falseto 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.
7.7 Cookie Security
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
withCredentialsoption on the client must betruefor 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 onParentNamespaceinstances. 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 toBroadcastOperator, ensuringio.emit()andsocket.broadcast.emit()are type-checked.DecorateAcknowledgements— transforms event map types to supportemitWithAck()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
transportsoption 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
emitWithAckmemory leak (issue #4984) suggests that pending ack tracking should be tied to socket lifecycle. - Connection state recovery's default
skipMiddlewares: trueis a security concern that arguably should default tofalse.
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:
HttpRequestandHttpResponseare 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 roomsdel(id: SocketId, room: Room)— remove a socket from a roomdelAll(id: SocketId)— remove a socket from all roomsbroadcast(packet, opts: BroadcastOptions)— broadcast a packetfetchSockets(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:
- Enable debug logging:
DEBUG=socket.io:*,engine:* node server.js - Check that the
pathoption matches between client and server (default:/socket.io). - Verify CORS configuration if client and server are on different origins.
- Check that the server port is accessible and not firewalled.
- If behind a reverse proxy, ensure it forwards the
UpgradeandConnectionheaders.
Warning
Issue #5288: Firefox may throw CORS errors during reconnection when the
Originheader changes between the initial connection and reconnection. Ensure your CORS configuration handles all valid origins consistently. The error manifests asconnect_errorwith a generic CORS message.
Frequent disconnections
Symptoms: Clients disconnect and reconnect repeatedly.
Diagnosis:
- Check
pingTimeoutandpingInterval. If the server is under heavy load, it may not send pings on time. IncreasepingTimeout. - Check for timer reliability issues. Node.js timers are not guaranteed to fire exactly on time under load (issue #5357).
- Check for proxy timeouts. Many reverse proxies (Nginx, CloudFlare, AWS ALB) have idle connection timeouts shorter than the heartbeat interval.
- 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
pingIntervalto send pings more frequently, or increasepingTimeoutto 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:
- Verify the adapter is correctly configured on all server instances.
- Check that the external message broker (Redis, Postgres) is accessible from all servers.
- Issue #5456:
ClusterAdapter.broadcast()can drop events whenpublishAndReturnOffsetthrows. Ensure error handling in the adapter layer. - 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
_placeholderproperty used for binary reconstruction gets renamed (e.g., toit). 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
maxDisconnectionDurationand message volume. - Large event payloads buffered during transport upgrade.
High CPU usage
Check for:
perMessageDeflate: true— compression is CPU-intensive.httpCompression: truewith 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
});