When to Use WebSockets
WebSockets maintain a persistent bidirectional connection between client and server. They are the right choice when the server needs to push data to clients without being polled — live auction prices, chat messages, collaborative editing, real-time notifications, and dashboard metrics. For everything else, HTTP with polling or Server-Sent Events is simpler and sufficient.
Socket.IO vs Raw WebSockets
Socket.IO builds on top of WebSockets and adds automatic reconnection, room-based broadcasting, event namespaces, and fallback to HTTP long-polling for clients that do not support WebSockets. For most production applications, the overhead of Socket.IO is worth the reliability guarantees. Use raw WebSockets only when you need maximum throughput and can handle reconnection logic yourself.
Basic Server Setup with NestJS
import { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets'
import { Server, Socket } from 'socket.io'
@WebSocketGateway({
cors: { origin: process.env.CLIENT_URL },
namespace: '/auction',
})
export class AuctionGateway {
@WebSocketServer()
server: Server
@SubscribeMessage('place-bid')
async handleBid(
@MessageBody() data: { auctionId: string; amount: number },
@ConnectedSocket() client: Socket,
) {
const result = await this.auctionService.placeBid(data)
// Broadcast new price to all clients in this auction room
this.server.to(data.auctionId).emit('bid-placed', result)
}
handleConnection(client: Socket) {
const auctionId = client.handshake.query.auctionId as string
client.join(auctionId)
}
}
Scaling Across Multiple Servers with Redis Pub/Sub
A single WebSocket server can handle thousands of connections, but eventually you need multiple servers for redundancy and throughput. The problem: a message emitted on Server A will not reach clients connected to Server B. Redis pub/sub solves this by acting as a message bus between servers.
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))
With the Redis adapter, when Server A calls io.to(roomId).emit(event, data), it publishes to Redis. All other servers subscribed to that channel receive the message and forward it to their locally connected clients in that room.
Connection State and Authentication
Authenticate WebSocket connections using the same JWT you use for HTTP requests. Validate the token during the handshake — not per-message, which is too expensive.
io.use(async (socket, next) => {
const token = socket.handshake.auth.token
try {
const payload = jwt.verify(token, process.env.JWT_SECRET)
socket.data.userId = payload.sub
next()
} catch {
next(new Error('Unauthorized'))
}
})
Store minimal state on the socket object (socket.data). For anything that needs to survive a reconnect, use a database or Redis, not in-memory socket state.
Client-Side Reconnection and State Recovery
import { io } from 'socket.io-client'
const socket = io(process.env.NEXT_PUBLIC_WS_URL, {
auth: { token: getAuthToken() },
reconnectionAttempts: 5,
reconnectionDelay: 1000,
reconnectionDelayMax: 10000,
})
socket.on('connect', () => {
// Re-subscribe to rooms and request missed updates
socket.emit('rejoin', { lastEventId: localState.lastEventId })
})
socket.on('connect_error', (err) => {
console.error('WebSocket error:', err.message)
})
Always handle the reconnection case in your UI. A user who reconnects after a network dropout should receive any events they missed — emit a rejoin event and let the server replay recent history from a database or Redis stream.
Production Checklist
- Set a maximum connections per server limit and monitor it — a connection leak will exhaust file descriptors
- Implement heartbeat/ping-pong to detect zombie connections (Socket.IO does this automatically)
- Use namespaces to separate concerns (
/auction,/notifications) rather than one giant socket - Never broadcast to all connected clients (
io.emit) in high-traffic systems — always target rooms or specific sockets - Log connection and disconnection events with the client IP for debugging and abuse detection