socket.io review
socket.io 4.8.3 is the Node server for the Socket.IO event protocol. It sits on Engine.IO, begins with HTTP long polling when necessary, upgrades to WebSocket when possible, and gives application code named events, acknowledgements, rooms, namespaces, middleware, broadcasts, and reconnection state. A `socket.io-client` peer speaks its packet format; a generic WebSocket client does not. The 4.8.3 release makes repeated shutdown safe by preventing `io.close()` from throwing after the server has already stopped. Our install loaded through both CommonJS and ESM. The package belongs on a persistent backend, since its transports, heartbeat, room membership, and optional recovery state all assume a process that remains available after the HTTP handshake.
socket.io 4.8.3 installed in 1.9 seconds, used 6 MB across 22 packages, loaded through CommonJS and ESM, and returned 0 audit findings in our sandbox; its browser bundle failed because this is server code. Use it when controlled clients need its rooms, acknowledgements, fallback, and reconnect protocol, and use plain WebSocket tooling when protocol interoperability matters.
We installed it
| Install | ✓ · 1.9s | 22 packages on disk · 6 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package with exports map |
| Browser | n/a | could not be bundled for the browser (Node-only code, most likely) |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does socket.io install cleanly?
Yes. In a fresh container with an empty cache, npm install socket.io finished in 2 seconds, leaving 22 packages and 6 MB on disk. npm audit reported no known vulnerabilities.
Can socket.io run in a browser?
Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.
Does socket.io work with both ESM and CommonJS?
Yes. Both import 'socket.io' and require('socket.io') worked in Node 22 in our run. The package is published as CommonJS with an exports map.
Does socket.io include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
socket.io or ws: which should you use?
ws: Choose it for standards-based WebSocket frames when the application can define reconnects, heartbeats, rooms, and message semantics. socket.io 4.8.3 installed in 1.9 seconds, used 6 MB across 22 packages, loaded through CommonJS and ESM, and returned 0 audit findings in our sandbox; its browser bundle failed because this is server code.
When should you not use socket.io?
Third-party clients must connect with the standard WebSocket protocol. Socket.IO uses an additional handshake and packet format that generic WebSocket libraries do not understand.
Use it if
- Your own web or mobile clients need rooms, named events, acknowledgements, and reconnection behavior under one protocol.
- Some networks require HTTP long-polling fallback before a WebSocket upgrade can succeed.
- Per-namespace middleware and room broadcasts map directly to tenant, document, game, or channel boundaries.
- The deployment can keep connections open and provide sticky routing plus a shared adapter when it runs more than one node.
- Third-party clients must connect with the standard WebSocket protocol. Socket.IO uses an additional handshake and packet format that generic WebSocket libraries do not understand.
- The service only moves raw text or binary frames and can implement its own heartbeats and reconnect policy. `ws` has a smaller protocol surface for that job.
- The hosting platform freezes or destroys the process after each HTTP request. Long polling, heartbeats, and upgraded connections require a live server instance.
- A multi-node setup cannot provide affinity for polling requests or a shared adapter for room state. An adapter distributes broadcasts, but it does not configure the load balancer.
- Compile-time event types are being treated as payload security. Bundled TypeScript declarations check local code and perform no runtime validation on data received from a client.
Setup reality
We installed socket.io 4.8.3 in a fresh Node 22 Bookworm sandbox. npm finished in 1.9 seconds, left 22 packages and 6 MB on disk, and reported 0 known vulnerabilities at critical, high, moderate, and low severity. The server package declares 7 direct dependencies, 0 peer dependencies, and 1452 KB unpacked. It carries the MIT license and includes TypeScript declarations.
The package is CommonJS with an exports map. require() and ESM import both worked in our sandbox. Our esbuild browser build failed on Node-only code, which confirms that this package stays on the server. Browser code installs socket.io-client. A plain WebSocket object cannot replace that client because it lacks the Socket.IO handshake, framing, acknowledgements, and namespace rules.
Attach Server to the HTTP or HTTPS server that owns the port. Browser origins need an explicit CORS policy, especially when cookies or credentials cross origins. Authentication commonly reads socket.handshake.auth in namespace middleware. Validate the token before room membership, store the resulting user identity in socket.data, and validate each event payload at runtime. The connection's socket.id may change after reconnection.
One Node process keeps rooms in memory. Several processes need a compatible adapter for cross-node broadcasts. If long polling remains enabled, the load balancer also needs sticky routing so every request from one Engine.IO session reaches the same node. Acknowledgements need timeouts because a handler may never call its callback. Connection-state recovery has a bounded window and adapter requirements, so clients still need a full resync path even though our 1.9-second install was uneventful.
Patterns
Attach Socket.IO to an HTTP server attach-http-server
import { createServer } from "node:http";
import { Server } from "socket.io";
const httpServer = createServer();
const io = new Server(httpServer, {
cors: { origin: "https://app.example.com", credentials: true },
});
io.on("connection", (socket) => {
socket.emit("ready", { connectionId: socket.id });
});
httpServer.listen(3000);The HTTP server owns the listening port. Credentialed browser requests require a specific allowed origin; `*` cannot be paired with credentials.
Reject an unauthenticated namespace connection authenticate-handshake
io.use(async (socket, next) => {
try {
const user = await verifyAccessToken(socket.handshake.auth.token);
socket.data.userId = user.id;
next();
} catch {
next(new Error("unauthorized"));
}
});Calling `next(error)` produces `connect_error` on the client. Authenticate before joining rooms, then authorize each state-changing event separately.
Validate data before changing state validate-event-payload
import { z } from "zod";
const Message = z.object({ roomId: z.string().uuid(), text: z.string().min(1).max(2000) });
io.on("connection", (socket) => {
socket.on("message:create", async (input, acknowledge) => {
const parsed = Message.safeParse(input);
if (!parsed.success) return acknowledge({ ok: false, code: "bad_payload" });
const message = await saveMessage(socket.data.userId, parsed.data);
acknowledge({ ok: true, message });
});
});Socket.IO's TypeScript event declarations disappear at runtime. Every network payload still needs shape checks and authorization before a write.
Join a room and broadcast to its members join-room
io.on("connection", (socket) => {
socket.on("document:open", async (documentId) => {
await requireDocumentAccess(socket.data.userId, documentId);
await socket.join(`document:${documentId}`);
io.to(`document:${documentId}`).emit("presence:joined", {
userId: socket.data.userId,
});
});
});`io.to(room)` includes the socket that just joined. Start the broadcast with `socket.to(room)` when the sender already updated its own UI.
Send an update only to peers exclude-sender
socket.on("cursor:move", ({ documentId, x, y }) => {
socket.to(`document:${documentId}`).volatile.emit("cursor:moved", {
userId: socket.data.userId,
x,
y,
});
});`socket.to(room)` excludes the sender. `volatile` also permits dropping the event when the transport is not writable, which fits replaceable cursor positions.
Bound an acknowledgement wait acknowledge-with-timeout
try {
const result = await socket
.timeout(5_000)
.emitWithAck("order:create", { sku: "ink-42", quantity: 2 });
console.log(result.orderId);
} catch {
console.error("server did not acknowledge within 5 seconds");
}The receiver must call its acknowledgement callback on every handled path. A timeout rejects the promise; it does not prove whether the server committed the operation.
Give administrators a separate namespace separate-namespace
const admin = io.of("/admin");
admin.use(requireAdmin);
admin.on("connection", (socket) => {
socket.on("jobs:list", async (acknowledge) => {
acknowledge(await listJobs());
});
});Each namespace has separate middleware, rooms, sockets, and broadcasts. A room name used under `/admin` does not refer to the same room in `/`.
Type client and server event names type-event-contract
interface ClientEvents {
"message:create": (
input: { roomId: string; text: string },
ack: (result: { ok: boolean }) => void
) => void;
}
interface ServerEvents {
"message:created": (message: { id: string; text: string }) => void;
}
const io = new Server<ClientEvents, ServerEvents>(httpServer);The generics catch local spelling and signature mistakes during compilation. They do not validate packets created by a compromised or outdated client.
Recover a short interrupted session enable-state-recovery
const io = new Server(httpServer, {
connectionStateRecovery: {
maxDisconnectionDuration: 120_000,
skipMiddlewares: false,
},
});
io.on("connection", (socket) => {
socket.emit("recovery:status", { recovered: socket.recovered });
});Recovery works only inside the 120-second window and depends on adapter support. Keeping middleware enabled rechecks authentication; expired sessions need a full state fetch.
Share broadcasts through Redis install-redis-adapter
import { createClient } from "redis";
import { createAdapter } from "@socket.io/redis-adapter";
const publisher = createClient({ url: process.env.REDIS_URL });
const subscriber = publisher.duplicate();
await Promise.all([publisher.connect(), subscriber.connect()]);
io.adapter(createAdapter(publisher, subscriber));The adapter carries broadcasts and room operations between Socket.IO nodes. Polling requests still require sticky routing at the load balancer.
Read sockets in a room across the adapter inspect-room-members
const sockets = await io.in("support:priority").fetchSockets();
for (const remoteSocket of sockets) {
console.log(remoteSocket.id, remoteSocket.data.userId);
}With a cluster adapter, `fetchSockets()` can query other nodes and may involve network work. Do not run it on every high-frequency event.
Close the server during shutdown shutdown-idempotently
async function shutdown() {
await new Promise((resolve) => io.close(resolve));
}
process.once("SIGTERM", shutdown);
process.once("SIGINT", shutdown);Version 4.8.3 fixes a throw when `io.close()` is called after the server has already stopped. Application shutdown should still register handlers once and close other resources explicitly.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ws | npm | Choose it for standards-based WebSocket frames when the application can define reconnects, heartbeats, rooms, and message semantics. |
| engine.io | npm | Choose the transport and upgrade layer alone when you want polling fallback while designing the higher-level event protocol yourself. |
| @fastify/websocket | npm | Choose it when a Fastify service needs WebSocket routes integrated with Fastify hooks and request handling. |
More web backend guides
urllib3 · requests · ws · anyio · undici · httpx · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

