NodeJSSocket.IO

Socket.IO

Socket.IO is a library that builds on WebSockets and adds everything you'd otherwise hand-roll: automatic reconnection, fallback transports, named events, rooms for grouping clients, broadcasting helpers, acknowledgements, and a Redis adapter for scaling across instances. It trades the rawness of ws for a rich, productive API — at the cost of a custom protocol (client and server must both be Socket.IO). This page covers the event-based model, rooms and namespaces, broadcasting, acknowledgements, scaling with the Redis adapter, and how it differs from plain WebSockets.

Socket.IO is not just WebSockets
Socket.IO uses its own protocol — you can't connect a raw WebSocket client to a Socket.IO server (or vice versa)
A frequent confusion: Socket.IO is **not** a plain WebSocket implementation. It layers its own protocol (the Engine.IO transport plus a message framing/event layer) *on top of* WebSockets, with automatic fallback to HTTP long-polling when WebSockets aren't available. The practical consequence is that **both ends must use Socket.IO** — you cannot point a browser's native `WebSocket` or a raw `ws` client at a Socket.IO server, nor connect a Socket.IO client to a bare WebSocket server. Choose Socket.IO when you want its features and control both client and server; choose raw [`ws`](/nodejs/websockets) when you need a standard WebSocket or are integrating with non-Socket.IO clients.
Setup — server and client

Bash
npm install socket.io          # server
# client (browser): npm install socket.io-client

server

JS
import { createServer } from 'node:http'
import { Server } from 'socket.io'

const httpServer = createServer()
const io = new Server(httpServer, {
  cors: { origin: 'https://app.example.com' },   // restrict allowed origins
})

io.on('connection', (socket) => {
  console.log('connected:', socket.id)

  // Listen for a NAMED event from this client:
  socket.on('chat:message', (payload) => {
    io.emit('chat:message', { user: socket.id, text: payload.text })   // to everyone
  })

  socket.on('disconnect', (reason) => console.log('left:', socket.id, reason))
})

httpServer.listen(3000)

client (browser)

JS
import { io } from 'socket.io-client'
const socket = io('https://api.example.com')

socket.on('connect', () => console.log('connected as', socket.id))
socket.emit('chat:message', { text: 'hello' })       // send a named event
socket.on('chat:message', (msg) => render(msg))       // receive named events
// Reconnection, transport fallback, and buffering are handled automatically.
Communication is by named events (`emit`/`on`), not raw messages — and reconnection is automatic
Where raw WebSockets give you a single `message` channel you must multiplex yourself, Socket.IO is **event-based**: `socket.emit('event:name', payload)` sends a named event with a structured (auto-serialized) payload, and `socket.on('event:name', handler)` receives it. This makes protocols self-documenting and easy to organize. On top of that, the client transparently handles **reconnection** (with backoff), **transport fallback** (long-polling if WebSockets fail), and **buffering** of events emitted while disconnected — all the resilience plumbing you'd otherwise build by hand on raw `ws`. That convenience is the main reason teams reach for Socket.IO.
Rooms — group clients for targeted broadcasts

JS
io.on('connection', (socket) => {
  // A socket can join any number of rooms (arbitrary string names):
  socket.on('room:join', (roomId) => {
    socket.join(roomId)
    socket.to(roomId).emit('system', `${socket.id} joined`)   // others in the room
  })

  socket.on('room:message', ({ roomId, text }) => {
    // Broadcast to everyone in the room INCLUDING or EXCLUDING the sender:
    io.to(roomId).emit('room:message', { from: socket.id, text })   // include sender
    // socket.to(roomId).emit(...)  → everyone in room EXCEPT the sender
  })
})
Rooms are server-side groupings — `io.to(room).emit()` targets just that group, no manual bookkeeping
A **room** is a named group of sockets maintained entirely server-side (clients don't know about rooms directly). A socket `join`s rooms, and you broadcast to a room with `io.to(roomId).emit(...)` — Socket.IO tracks membership and delivers only to those sockets. This is exactly the per-channel targeting you'd otherwise hand-build on raw [`ws`](/nodejs/websockets) by maintaining your own socket-to-group maps. Rooms make chat channels, per-document collaboration sessions, per-user notification streams (each user in a room named by their ID), and game lobbies trivial. **Namespaces** (`io.of('/admin')`) go further, partitioning the entire connection into separate logical endpoints with their own middleware and events.
Acknowledgements — request/response over events

JS
// Client emits with a callback — the server invokes it to acknowledge/reply:
socket.emit('order:create', { item: 'book' }, (response) => {
  console.log('server acked:', response.orderId)   // got the result back
})

// Server receives the ack callback as the last argument:
io.on('connection', (socket) => {
  socket.on('order:create', async (data, ack) => {
    const order = await createOrder(data)
    ack({ ok: true, orderId: order.id })           // call it to "respond"
  })
})
Acknowledgement callbacks give you request/response semantics on top of fire-and-forget events
Events are normally fire-and-forget, but Socket.IO supports **acknowledgements**: the emitter passes a callback as the last argument, and the receiver calls it to send a response back — giving you request/response semantics over the event channel, useful when the client needs confirmation or a result (the created order's ID, a validation outcome). You can also set a timeout on the ack so a missing response doesn't hang forever. This bridges the gap between pure event streaming and the round-trip behaviour you'd get from an HTTP call, without leaving the persistent connection.
Scaling with the Redis adapter

Bash
npm install @socket.io/redis-adapter redis

JS
import { createAdapter } from '@socket.io/redis-adapter'
import { createClient } from 'redis'

const pubClient = createClient({ url: process.env.REDIS_URL })
const subClient = pubClient.duplicate()
await Promise.all([pubClient.connect(), subClient.connect()])

io.adapter(createAdapter(pubClient, subClient))
// Now io.to(room).emit(...) reaches clients on EVERY instance, not just this one.
Without the Redis adapter, broadcasts only reach clients on the SAME instance — rooms break across a cluster
By default a Socket.IO server only knows about the connections on *its own* process, so once you run multiple instances behind a load balancer, `io.to(room).emit()` reaches only the room members connected to *that* instance — users on other instances miss the message, and rooms appear broken. The **Redis adapter** fixes this by using [Redis](/nodejs/redis) pub/sub as a backplane: each instance publishes broadcasts to Redis and relays received ones to its local sockets, so emits and room broadcasts fan out across **all** instances. This is the standard way to [scale](/nodejs/scaling-strategies) Socket.IO horizontally. You'll also typically enable sticky sessions at the load balancer so a client's HTTP-polling fallback and upgrade requests reach the same instance during the handshake.
Socket.IO vs raw ws

Socket.IO

Raw ws

Reconnection

Automatic with backoff

You implement it

Fallback transport

HTTP long-polling if WS fails

None — WS only

Rooms / broadcasting

Built in

You maintain the maps

Named events + acks

Built in

You design the protocol

Multi-instance scaling

Redis adapter

You build the backplane

Protocol / interop

Custom — both ends must be Socket.IO

Standard WebSocket, interoperable

Overhead

Larger client, more abstraction

Minimal, closer to the metal

Next
When you only need server-to-client streaming, there's a simpler option: [Server-Sent Events (SSE)](/nodejs/server-sent-events).