DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

Socket.io Complete Guide: Real-Time Apps in 2026

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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') }
})
Enter fullscreen mode Exit fullscreen mode

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)
  })
})
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Scaling with Redis Adapter

npm install @socket.io/redis-adapter ioredis
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

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 } })
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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)