socket.io
socket.io is an event-based realtime server for Node.js. It runs its own protocol on top of Engine.IO, giving you WebSocket transport with an HTTP long-polling fallback, automatic reconnection, rooms for broadcasting to groups of clients, namespaces for splitting one server into channels, and per-event acknowledgements. Browsers connect with the matching socket.io-client package; it is not a plain WebSocket server and plain WebSocket clients cannot talk to it.
Still the fastest path to rooms, reconnection, and fallback transports in Node, and the v4 API has been calm for years. Pick plain ws when you control every client and can afford to rebuild the conveniences, and think twice on serverless.
Use it if
- You need rooms, broadcasts, and presence semantics out of the box for chat, live dashboards, or collaborative features
- Your clients sit behind corporate proxies or strict firewalls and you want long-polling fallback plus reconnection handled for you
- You want request-response semantics over a persistent connection: acknowledgements and emitWithAck with timeouts are built in
- You do not control every client: it speaks its own protocol, so plain WebSocket clients, curl, or services in other languages need a Socket.IO client port that may lag or not exist
- You care about per-connection overhead: the ws package is much leaner if you can own reconnection and message framing yourself
- You deploy on serverless platforms: long-lived connections and per-node state are a poor fit for functions that scale to zero
- You plan to scale horizontally casually: multi-node requires sticky sessions plus an adapter like Redis, and skipping either produces confusing intermittent failures rather than a clean error
Setup reality
npm install socket.io and attach it to an HTTP server; the basic case works in minutes. The friction points: CORS must be configured explicitly on the server or browser clients fail with opaque connection errors; the client and server major versions must match because v2, v3, and v4 protocols are incompatible; and the moment you run more than one node you need sticky sessions at the load balancer plus an adapter (Redis is the usual one) or long-polling requests land on the wrong instance. The docs troubleshooting guide exists because these failure modes are common.
Patterns
Create a server and echo eventsbasic-server
import { createServer } from 'http';
import { Server } from 'socket.io';
const httpServer = createServer();
const io = new Server(httpServer, {
cors: { origin: 'https://app.example.com' },
});
io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
io.emit('chat message', msg); // to everyone, sender included
});
});
httpServer.listen(3000);Without the cors option, browser clients on another origin fail during the handshake with a generic xhr poll error.
Connect from the browser and handle reconnectsclient-connect
import { io } from 'socket.io-client';
const socket = io('https://api.example.com', {
auth: { token: 'abc' },
});
socket.on('connect', () => console.log(socket.id));
socket.on('disconnect', (reason) => console.log('lost:', reason));
socket.io.on('reconnect', (attempt) => console.log('back after', attempt));Reconnection is on by default; socket.id changes after every reconnect, so never use it as a stable user identifier.
Join a room and emit to itrooms-broadcast
io.on('connection', (socket) => {
socket.join('room-42');
// everyone in the room, including this socket
io.to('room-42').emit('update', payload);
// everyone in the room EXCEPT this socket
socket.to('room-42').emit('user joined', socket.id);
});io.to() includes the sender, socket.to() excludes it; mixing them up is the classic duplicate-message bug.
Split the server into namespacesnamespaces
const adminNs = io.of('/admin');
adminNs.on('connection', (socket) => {
adminNs.emit('hello', 'admins only');
});
// client side
const adminSocket = io('https://api.example.com/admin');Namespaces share one underlying connection; rooms exist per namespace, so 'room-42' in /admin is not 'room-42' in /.
Authenticate connections with middlewareauth-middleware
io.use((socket, next) => {
const token = socket.handshake.auth.token;
verifyToken(token)
.then((user) => {
socket.data.user = user;
next();
})
.catch(() => next(new Error('unauthorized')));
});Errors passed to next() reach the client as a connect_error event; anything on socket.data survives for the connection lifetime.
Get a response for an emitted eventacknowledgements
// client
const response = await socket
.timeout(5000)
.emitWithAck('create-order', { sku: 'x1' });
// server
socket.on('create-order', async (data, callback) => {
const order = await createOrder(data);
callback({ ok: true, id: order.id });
});Without .timeout(), a server that never calls the callback leaves the client promise hanging forever.
Broadcast to everyone except the senderbroadcast-except-sender
io.on('connection', (socket) => {
socket.on('typing', (user) => {
socket.broadcast.emit('typing', user);
});
});socket.broadcast covers the whole namespace; chain .to(room) to narrow it to one room.
Clean up on disconnect and inspect the reasondisconnect-handling
io.on('connection', (socket) => {
socket.on('disconnect', (reason) => {
// e.g. 'transport close', 'ping timeout', 'server namespace disconnect'
removeFromPresence(socket.data.user, reason);
});
});A brief network blip fires disconnect then a fresh connection event; debounce presence updates or users flicker offline.
Scale across multiple nodes with Redisscale-redis-adapter
import { createClient } from 'redis';
import { createAdapter } from '@socket.io/redis-adapter';
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
const io = new Server(httpServer, {
adapter: createAdapter(pubClient, subClient),
});The adapter routes broadcasts between nodes, but you still need sticky sessions at the load balancer for long-polling clients.
Type events end to end in TypeScripttyped-events
interface ClientToServerEvents {
'chat message': (msg: string) => void;
}
interface ServerToClientEvents {
'chat message': (msg: string) => void;
}
const io = new Server<ClientToServerEvents, ServerToClientEvents>(httpServer);
io.on('connection', (socket) => {
socket.on('chat message', (msg) => io.emit('chat message', msg));
});The generics only type your own code; nothing validates payloads at runtime, so still parse untrusted input.