Socket.io wraps WebSockets with automatic reconnection, rooms, namespaces, event broadcasting, and a Redis Adapter for horizontal scaling. For most real-time use cases — chat, notifications, live dashboards — it's still the most complete off-the-shelf solution for Node.js.
Installation
npm install socket.io socket.io-client
Server Setup
import { createServer } from 'http'
import { Server } from 'socket.io'
import express from 'express'
const app = express()
const httpServer = createServer(app)
const io = new Server(httpServer, {
cors: { origin: process.env.CLIENT_URL, credentials: true },
transports: ['websocket', 'polling']
})
io.on('connection', (socket) => {
console.log(`Connected: ${socket.id}`)
socket.on('disconnect', (reason) => console.log(`Disconnected: ${reason}`))
})
httpServer.listen(3000)
Typed Events
interface ClientToServerEvents {
'chat:message': (data: { roomId: string; content: string }) => void
'room:join': (roomId: string, callback: (res: { success: boolean }) => void) => void
'room:leave': (roomId: string) => void
}
interface ServerToClientEvents {
'chat:message': (message: Message) => void
'user:joined': (data: { userId: string; username: string }) => void
'error': (data: { code: string; message: string }) => void
}
interface SocketData {
userId: string
username: string
}
const io = new Server<ClientToServerEvents, ServerToClientEvents, {}, SocketData>(httpServer)
JWT Authentication
import jwt from 'jsonwebtoken'
io.use(async (socket, next) => {
const token = socket.handshake.auth.token
if (!token) return next(new Error('Authentication required'))
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; username: string }
socket.data.userId = payload.sub
socket.data.username = payload.username
next()
} catch {
next(new Error('Invalid token'))
}
})
// Client
const socket = io('http://localhost:3000', {
auth: { token: localStorage.getItem('accessToken') }
})
Rooms
io.on('connection', (socket) => {
const { userId, username } = socket.data
socket.on('room:join', async (roomId, callback) => {
const canJoin = await canUserJoinRoom(userId, roomId)
if (!canJoin) {
callback({ success: false })
return socket.emit('error', { code: 'FORBIDDEN', message: 'Cannot join room' })
}
await socket.join(roomId)
socket.to(roomId).emit('user:joined', { userId, username })
callback({ success: true })
})
socket.on('room:leave', (roomId) => {
socket.leave(roomId)
})
})
Chat Messages
socket.on('chat:message', async ({ roomId, content }) => {
if (!socket.rooms.has(roomId)) {
return socket.emit('error', { code: 'NOT_IN_ROOM', message: 'Join the room first' })
}
const trimmed = content.trim()
if (!trimmed || trimmed.length > 2000) return
const message = await db.message.create({
data: { content: trimmed, authorId: socket.data.userId, roomId },
include: { author: { select: { id: true, username: true } } }
})
io.to(roomId).emit('chat:message', message)
})
Broadcasting Patterns
io.to(roomId).emit('msg', data) // everyone in room
socket.to(roomId).emit('msg', data) // room EXCEPT sender
io.to(socketId).emit('dm', data) // specific client
io.to('r1').to('r2').emit('msg', data) // multiple rooms
io.emit('broadcast', data) // everyone connected
io.except(roomId).emit('event', data) // everyone NOT in room
Scaling with Redis Adapter
npm install @socket.io/redis-adapter ioredis
import { createAdapter } from '@socket.io/redis-adapter'
import { Redis } from 'ioredis'
const pubClient = new Redis(process.env.REDIS_URL!)
const subClient = pubClient.duplicate()
io.adapter(createAdapter(pubClient, subClient))
// io.to(roomId).emit() now works across all server instances
React Client Hook
export function useSocket(token: string | null) {
const [connected, setConnected] = useState(false)
const socketRef = useRef<Socket | null>(null)
useEffect(() => {
if (!token) return
const socket = io(process.env.NEXT_PUBLIC_SOCKET_URL!, {
auth: { token },
transports: ['websocket']
})
socket.on('connect', () => setConnected(true))
socket.on('disconnect', () => setConnected(false))
socketRef.current = socket
return () => { socket.disconnect() }
}, [token])
return { socket: socketRef.current, connected }
}
Namespaces
// Separate channel for admin dashboard
const adminNs = io.of('/admin')
adminNs.use(async (socket, next) => {
const user = await verifyAdminToken(socket.handshake.auth.token)
if (!user?.isAdmin) return next(new Error('Admin only'))
next()
})
adminNs.on('connection', (socket) => {
socket.join('metrics')
// emit real-time metrics to admin dashboard
})
// Client
const adminSocket = io('http://localhost:3000/admin', { auth: { token } })
Common Pitfalls
Memory leaks:
// Bad — adds listener every render
useEffect(() => { socket.on('event', handler) })
// Good — cleanup on unmount
useEffect(() => {
socket.on('event', handler)
return () => socket.off('event', handler)
}, [socket])
Missing room validation: Always check socket.rooms.has(roomId) before accepting room-scoped events.
Broadcasting storms: Avoid io.emit() for large audiences — always scope to rooms.
Full article at stacknotice.com/blog/socketio-complete-guide-2026
Top comments (0)