A comprehensive exploration of WebSocket protocol, its history, core concepts, and practical implementation with NestJS. Learn when WebSocket provides superior real-time capabilities over HTTP polling, SSE, and other alternatives.

In the era of real-time applications, WebSocket has become the backbone of modern interactive experiences. From chat applications and collaborative editing to live dashboards and multiplayer games, WebSocket enables the instant, bidirectional communication that users expect.
Before WebSocket, developers relied on HTTP polling, long-polling, or server-sent events—each with significant limitations. WebSocket changed everything by providing a persistent, full-duplex connection over a single TCP socket. No more polling overhead, no more half-duplex constraints, just pure real-time communication.
This deep dive explores WebSocket's architecture, compares it against alternative real-time protocols, and demonstrates a production-grade implementation using NestJS. If you've ever struggled with scaling real-time features or wondered when to choose WebSocket over alternatives, this article provides the answers.
WebSocket emerged in 2008 when developers were frustrated with HTTP's request-response model for real-time applications. The web needed a way to push data from server to client without constant polling, which wasted bandwidth and increased latency.
Traditional HTTP communication has fundamental limitations for real-time applications:
HTTP Polling: Client repeatedly requests updates at fixed intervals. Wastes bandwidth, increases server load, and introduces latency. If you poll every second, you're making 3,600 requests per hour per client.
Long Polling: Client makes a request, server holds it open until data is available. Better than polling, but still creates new connections constantly, consuming resources.
Server-Sent Events (SSE): Server pushes data to client over HTTP. Unidirectional only—client can't send data through the same connection. Limited to 6 concurrent connections per browser.
HTTP/2 Server Push: Server can push resources, but it's designed for assets, not application data. Complex to implement for real-time features.
WebSocket eliminates these problems by establishing a persistent, bidirectional connection. After the initial HTTP handshake, the connection upgrades to WebSocket protocol, enabling both client and server to send messages freely.
WebSocket introduced several concepts that transformed real-time web development:
Understanding when to use WebSocket requires comparing it against alternatives.
HTTP polling is the simplest but least efficient approach to real-time updates.
| Aspect | WebSocket | HTTP Polling |
|---|---|---|
| Connection | Persistent | New per request |
| Latency | Very low (~1-10ms) | High (poll interval) |
| Bandwidth | Minimal overhead | High (headers per request) |
| Server Load | Low | High (constant requests) |
| Bidirectional | Yes | No (separate requests) |
| Real-time | True real-time | Delayed by interval |
| Complexity | Moderate | Simple |
| Scalability | Excellent | Poor |
Use WebSocket when: You need true real-time updates with minimal latency and efficient resource usage.
Use HTTP Polling when: You need simple implementation for infrequent updates and have few concurrent users.
SSE provides server-to-client streaming over HTTP, but with limitations.
| Aspect | WebSocket | SSE |
|---|---|---|
| Direction | Bidirectional | Server to client only |
| Protocol | WebSocket | HTTP |
| Data Format | Text or binary | Text only (UTF-8) |
| Browser Support | Excellent | Good (no IE) |
| Connection Limit | Unlimited | 6 per domain |
| Reconnection | Manual | Automatic |
| Proxy Support | Can be blocked | Better (HTTP) |
| Use Case | Chat, gaming | Notifications, feeds |
Use WebSocket when: You need bidirectional communication or binary data transfer.
Use SSE when: You only need server-to-client updates and want automatic reconnection with simpler implementation.
WebRTC is designed for peer-to-peer communication, particularly for audio/video.
| Aspect | WebSocket | WebRTC |
|---|---|---|
| Architecture | Client-server | Peer-to-peer |
| Latency | Very low | Ultra-low |
| Use Case | Application data | Audio/video/data |
| Setup Complexity | Simple | Complex (STUN/TURN) |
| NAT Traversal | Not needed | Required |
| Encryption | Optional (WSS) | Mandatory |
| Bandwidth | Low | High (media) |
| Browser Support | Excellent | Good |
Use WebSocket when: You need client-server real-time communication for application data.
Use WebRTC when: You need peer-to-peer audio/video streaming or ultra-low latency data channels.
gRPC supports bidirectional streaming over HTTP/2, competing with WebSocket for some use cases.
| Aspect | WebSocket | gRPC Streaming |
|---|---|---|
| Protocol | WebSocket | HTTP/2 |
| Data Format | Any | Protocol Buffers |
| Browser Support | Native | Requires proxy |
| Type Safety | Manual | Strong (Protobuf) |
| Tooling | Minimal | Extensive |
| Load Balancing | Challenging | Better support |
| Use Case | Web apps | Microservices |
Use WebSocket when: You're building browser-based real-time applications.
Use gRPC Streaming when: You're building microservices that need type-safe streaming with strong contracts.
WebSocket excels in specific scenarios where real-time bidirectional communication is essential.
WebSocket is the de facto standard for chat because:
Real-world example: Slack, Discord, and WhatsApp Web all use WebSocket for their core messaging functionality.
Real-time collaboration tools rely on WebSocket for:
Example: Google Docs, Figma, and Notion use WebSocket for collaborative features.
Monitoring systems use WebSocket for:
Example: Grafana, Datadog, and New Relic dashboards use WebSocket for live data.
Online games require WebSocket for:
Example: Agar.io, Slither.io, and browser-based multiplayer games.
Trading applications need WebSocket for:
Example: Binance, Coinbase Pro, and trading platforms use WebSocket for market data.
IoT systems use WebSocket for:
Example: Smart home dashboards, industrial IoT monitoring.
Understanding WebSocket requires grasping its fundamental building blocks.
WebSocket starts as an HTTP request that upgrades to WebSocket protocol:
Client Request:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.comServer Response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=After this handshake, the connection switches to WebSocket protocol. The Sec-WebSocket-Key and Sec-WebSocket-Accept headers prevent caching proxies from interfering.
WebSocket messages are sent in frames with specific structure:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+Key fields:
WebSocket defines several frame types:
WebSocket connections have distinct states:
WebSocket uses ping/pong frames to detect dead connections:
// Server sends ping
socket.ping();
// Client automatically responds with pong
// If no pong received within timeout, connection is deadThis prevents zombie connections from consuming resources.
WebSocket supports subprotocols for application-level protocols:
Sec-WebSocket-Protocol: chat, superchatServer selects one:
Sec-WebSocket-Protocol: chatCommon subprotocols include STOMP, MQTT over WebSocket, and custom protocols.
Let's build a production-grade WebSocket server using NestJS. We'll create a real-time chat application with rooms, private messages, and presence tracking.
First, install dependencies:
npm i -g @nestjs/cli
nest new websocket-chat-api
cd websocket-chat-apiNestJS uses Socket.IO by default, which provides WebSocket with fallbacks and additional features.
Build the main gateway for WebSocket connections:
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
MessageBody,
ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger, UseGuards } from '@nestjs/common';
import { WsAuthGuard } from './guards/ws-auth.guard';
interface User {
id: string;
username: string;
socketId: string;
}
interface Message {
id: string;
roomId: string;
userId: string;
username: string;
content: string;
timestamp: Date;
}
@WebSocketGateway({
cors: {
origin: '*', // Configure properly in production
},
namespace: '/chat',
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
private readonly logger = new Logger(ChatGateway.name);
private users: Map<string, User> = new Map();
private rooms: Map<string, Set<string>> = new Map();
async handleConnection(client: Socket) {
this.logger.log(`Client connected: ${client.id}`);
// Extract user info from handshake (in production, verify JWT)
const username = client.handshake.auth.username || `Guest${Math.floor(Math.random() * 1000)}`;
const userId = client.handshake.auth.userId || client.id;
const user: User = {
id: userId,
username,
socketId: client.id,
};
this.users.set(client.id, user);
// Notify all clients about new user
this.server.emit('user:connected', {
userId: user.id,
username: user.username,
});
// Send current online users to the new client
const onlineUsers = Array.from(this.users.values()).map(u => ({
id: u.id,
username: u.username,
}));
client.emit('users:list', onlineUsers);
this.logger.log(`User ${username} connected with socket ${client.id}`);
}
async handleDisconnect(client: Socket) {
const user = this.users.get(client.id);
if (user) {
// Remove user from all rooms
this.rooms.forEach((members, roomId) => {
if (members.has(client.id)) {
members.delete(client.id);
this.server.to(roomId).emit('user:left', {
roomId,
userId: user.id,
username: user.username,
});
}
});
this.users.delete(client.id);
// Notify all clients about user disconnect
this.server.emit('user:disconnected', {
userId: user.id,
username: user.username,
});
this.logger.log(`User ${user.username} disconnected`);
}
}
@SubscribeMessage('room:join')
handleJoinRoom(
@MessageBody() data: { roomId: string },
@ConnectedSocket() client: Socket,
) {
const user = this.users.get(client.id);
if (!user) {
return { error: 'User not found' };
}
// Join the room
client.join(data.roomId);
// Track room membership
if (!this.rooms.has(data.roomId)) {
this.rooms.set(data.roomId, new Set());
}
this.rooms.get(data.roomId).add(client.id);
// Notify room members
this.server.to(data.roomId).emit('user:joined', {
roomId: data.roomId,
userId: user.id,
username: user.username,
});
this.logger.log(`User ${user.username} joined room ${data.roomId}`);
return {
success: true,
roomId: data.roomId,
message: `Joined room ${data.roomId}`,
};
}
@SubscribeMessage('room:leave')
handleLeaveRoom(
@MessageBody() data: { roomId: string },
@ConnectedSocket() client: Socket,
) {
const user = this.users.get(client.id);
if (!user) {
return { error: 'User not found' };
}
// Leave the room
client.leave(data.roomId);
// Update room membership
const room = this.rooms.get(data.roomId);
if (room) {
room.delete(client.id);
if (room.size === 0) {
this.rooms.delete(data.roomId);
}
}
// Notify room members
this.server.to(data.roomId).emit('user:left', {
roomId: data.roomId,
userId: user.id,
username: user.username,
});
this.logger.log(`User ${user.username} left room ${data.roomId}`);
return {
success: true,
roomId: data.roomId,
message: `Left room ${data.roomId}`,
};
}
@SubscribeMessage('message:send')
handleMessage(
@MessageBody() data: { roomId: string; content: string },
@ConnectedSocket() client: Socket,
) {
const user = this.users.get(client.id);
if (!user) {
return { error: 'User not found' };
}
if (!data.content || data.content.trim().length === 0) {
return { error: 'Message content is required' };
}
const message: Message = {
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
roomId: data.roomId,
userId: user.id,
username: user.username,
content: data.content.trim(),
timestamp: new Date(),
};
// Broadcast message to room
this.server.to(data.roomId).emit('message:received', message);
this.logger.log(
`Message from ${user.username} in room ${data.roomId}: ${message.content.substring(0, 50)}`,
);
return {
success: true,
messageId: message.id,
};
}
@SubscribeMessage('message:private')
handlePrivateMessage(
@MessageBody() data: { recipientId: string; content: string },
@ConnectedSocket() client: Socket,
) {
const sender = this.users.get(client.id);
if (!sender) {
return { error: 'User not found' };
}
// Find recipient's socket
const recipient = Array.from(this.users.values()).find(
u => u.id === data.recipientId,
);
if (!recipient) {
return { error: 'Recipient not found' };
}
const message = {
id: `pm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
senderId: sender.id,
senderUsername: sender.username,
recipientId: recipient.id,
content: data.content.trim(),
timestamp: new Date(),
};
// Send to recipient
this.server.to(recipient.socketId).emit('message:private', message);
// Send confirmation to sender
client.emit('message:private:sent', message);
this.logger.log(
`Private message from ${sender.username} to ${recipient.username}`,
);
return {
success: true,
messageId: message.id,
};
}
@SubscribeMessage('typing:start')
handleTypingStart(
@MessageBody() data: { roomId: string },
@ConnectedSocket() client: Socket,
) {
const user = this.users.get(client.id);
if (user) {
client.to(data.roomId).emit('typing:start', {
roomId: data.roomId,
userId: user.id,
username: user.username,
});
}
}
@SubscribeMessage('typing:stop')
handleTypingStop(
@MessageBody() data: { roomId: string },
@ConnectedSocket() client: Socket,
) {
const user = this.users.get(client.id);
if (user) {
client.to(data.roomId).emit('typing:stop', {
roomId: data.roomId,
userId: user.id,
username: user.username,
});
}
}
// Admin method to get room info
getRoomInfo(roomId: string) {
const members = this.rooms.get(roomId);
if (!members) {
return null;
}
const users = Array.from(members)
.map(socketId => this.users.get(socketId))
.filter(Boolean);
return {
roomId,
memberCount: members.size,
members: users.map(u => ({ id: u.id, username: u.username })),
};
}
// Broadcast to all connected clients
broadcastToAll(event: string, data: any) {
this.server.emit(event, data);
}
// Broadcast to specific room
broadcastToRoom(roomId: string, event: string, data: any) {
this.server.to(roomId).emit(event, data);
}
}This gateway implements:
Secure WebSocket connections with authentication:
import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
@Injectable()
export class WsAuthGuard implements CanActivate {
private readonly logger = new Logger(WsAuthGuard.name);
canActivate(context: ExecutionContext): boolean {
const client: Socket = context.switchToWs().getClient();
const token = client.handshake.auth.token;
if (!token) {
this.logger.warn(`Connection rejected: No token provided`);
throw new WsException('Unauthorized');
}
try {
// Verify JWT token (simplified for demo)
// In production, use proper JWT verification
const decoded = this.verifyToken(token);
// Attach user info to socket
client.data.user = decoded;
return true;
} catch (error) {
this.logger.warn(`Connection rejected: Invalid token`);
throw new WsException('Unauthorized');
}
}
private verifyToken(token: string): any {
// Implement proper JWT verification
// This is a simplified example
try {
const payload = JSON.parse(
Buffer.from(token.split('.')[1], 'base64').toString()
);
return payload;
} catch {
throw new Error('Invalid token');
}
}
}Apply the guard to specific message handlers:
@UseGuards(WsAuthGuard)
@SubscribeMessage('message:send')
handleMessage(@MessageBody() data: any, @ConnectedSocket() client: Socket) {
// Handler code
}Separate business logic from gateway:
import { Injectable, Logger } from '@nestjs/common';
export interface ChatMessage {
id: string;
roomId: string;
userId: string;
username: string;
content: string;
timestamp: Date;
}
export interface ChatRoom {
id: string;
name: string;
createdAt: Date;
createdBy: string;
}
@Injectable()
export class ChatService {
private readonly logger = new Logger(ChatService.name);
// In-memory storage (use Redis or database in production)
private messages: Map<string, ChatMessage[]> = new Map();
private rooms: Map<string, ChatRoom> = new Map();
createRoom(id: string, name: string, createdBy: string): ChatRoom {
const room: ChatRoom = {
id,
name,
createdAt: new Date(),
createdBy,
};
this.rooms.set(id, room);
this.messages.set(id, []);
this.logger.log(`Room created: ${name} (${id})`);
return room;
}
getRoom(id: string): ChatRoom | undefined {
return this.rooms.get(id);
}
getAllRooms(): ChatRoom[] {
return Array.from(this.rooms.values());
}
deleteRoom(id: string): boolean {
const deleted = this.rooms.delete(id);
this.messages.delete(id);
if (deleted) {
this.logger.log(`Room deleted: ${id}`);
}
return deleted;
}
saveMessage(message: ChatMessage): void {
const roomMessages = this.messages.get(message.roomId) || [];
roomMessages.push(message);
this.messages.set(message.roomId, roomMessages);
// Keep only last 100 messages per room
if (roomMessages.length > 100) {
this.messages.set(message.roomId, roomMessages.slice(-100));
}
}
getMessages(roomId: string, limit: number = 50): ChatMessage[] {
const messages = this.messages.get(roomId) || [];
return messages.slice(-limit);
}
getMessageById(roomId: string, messageId: string): ChatMessage | undefined {
const messages = this.messages.get(roomId) || [];
return messages.find(m => m.id === messageId);
}
deleteMessage(roomId: string, messageId: string): boolean {
const messages = this.messages.get(roomId);
if (!messages) {
return false;
}
const index = messages.findIndex(m => m.id === messageId);
if (index === -1) {
return false;
}
messages.splice(index, 1);
this.logger.log(`Message deleted: ${messageId} from room ${roomId}`);
return true;
}
searchMessages(roomId: string, query: string): ChatMessage[] {
const messages = this.messages.get(roomId) || [];
const lowerQuery = query.toLowerCase();
return messages.filter(m =>
m.content.toLowerCase().includes(lowerQuery) ||
m.username.toLowerCase().includes(lowerQuery)
);
}
getMessageStats(roomId: string): {
total: number;
byUser: Record<string, number>;
firstMessage: Date | null;
lastMessage: Date | null;
} {
const messages = this.messages.get(roomId) || [];
const byUser: Record<string, number> = {};
messages.forEach(m => {
byUser[m.username] = (byUser[m.username] || 0) + 1;
});
return {
total: messages.length,
byUser,
firstMessage: messages[0]?.timestamp || null,
lastMessage: messages[messages.length - 1]?.timestamp || null,
};
}
}Add HTTP endpoints for chat management:
import {
Controller,
Get,
Post,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ChatService } from './chat.service';
import { ChatGateway } from './chat.gateway';
@Controller('chat')
export class ChatController {
constructor(
private readonly chatService: ChatService,
private readonly chatGateway: ChatGateway,
) {}
@Get('rooms')
getRooms() {
return {
rooms: this.chatService.getAllRooms(),
};
}
@Get('rooms/:id')
getRoom(@Param('id') id: string) {
const room = this.chatService.getRoom(id);
if (!room) {
return { error: 'Room not found' };
}
const info = this.chatGateway.getRoomInfo(id);
return {
room,
...info,
};
}
@Post('rooms')
@HttpCode(HttpStatus.CREATED)
createRoom(
@Body() body: { id: string; name: string; createdBy: string },
) {
const room = this.chatService.createRoom(
body.id,
body.name,
body.createdBy,
);
// Notify all connected clients
this.chatGateway.broadcastToAll('room:created', room);
return { room };
}
@Delete('rooms/:id')
@HttpCode(HttpStatus.NO_CONTENT)
deleteRoom(@Param('id') id: string) {
const deleted = this.chatService.deleteRoom(id);
if (deleted) {
// Notify all connected clients
this.chatGateway.broadcastToAll('room:deleted', { roomId: id });
}
return { success: deleted };
}
@Get('rooms/:id/messages')
getMessages(
@Param('id') id: string,
@Query('limit') limit?: string,
) {
const messages = this.chatService.getMessages(
id,
limit ? parseInt(limit, 10) : 50,
);
return { messages };
}
@Get('rooms/:id/messages/search')
searchMessages(
@Param('id') id: string,
@Query('q') query: string,
) {
if (!query) {
return { error: 'Query parameter is required' };
}
const messages = this.chatService.searchMessages(id, query);
return { messages, count: messages.length };
}
@Get('rooms/:id/stats')
getRoomStats(@Param('id') id: string) {
const stats = this.chatService.getMessageStats(id);
return { stats };
}
@Post('rooms/:id/broadcast')
@HttpCode(HttpStatus.OK)
broadcastToRoom(
@Param('id') id: string,
@Body() body: { event: string; data: any },
) {
this.chatGateway.broadcastToRoom(id, body.event, body.data);
return {
success: true,
message: `Broadcast sent to room ${id}`,
};
}
}Wire everything together:
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';
import { ChatService } from './chat.service';
import { ChatController } from './chat.controller';
@Module({
controllers: [ChatController],
providers: [ChatGateway, ChatService],
exports: [ChatGateway, ChatService],
})
export class ChatModule {}import { Module } from '@nestjs/common';
import { ChatModule } from './chat/chat.module';
@Module({
imports: [ChatModule],
})
export class AppModule {}Configure CORS for WebSocket:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable CORS for HTTP
app.enableCors({
origin: '*', // Configure properly in production
credentials: true,
});
await app.listen(3000);
console.log('WebSocket Chat Server running on http://localhost:3000');
console.log('WebSocket endpoint: ws://localhost:3000/chat');
}
bootstrap();Create a TypeScript client for the chat application:
import { io, Socket } from 'socket.io-client';
interface ChatClientOptions {
url: string;
username: string;
token?: string;
}
export class ChatClient {
private socket: Socket;
private username: string;
constructor(options: ChatClientOptions) {
this.username = options.username;
this.socket = io(`${options.url}/chat`, {
auth: {
username: options.username,
token: options.token,
},
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5,
});
this.setupEventHandlers();
}
private setupEventHandlers() {
this.socket.on('connect', () => {
console.log('Connected to chat server');
});
this.socket.on('disconnect', (reason) => {
console.log('Disconnected:', reason);
});
this.socket.on('connect_error', (error) => {
console.error('Connection error:', error.message);
});
this.socket.on('reconnect', (attemptNumber) => {
console.log('Reconnected after', attemptNumber, 'attempts');
});
}
// Join a room
joinRoom(roomId: string): Promise<any> {
return new Promise((resolve, reject) => {
this.socket.emit('room:join', { roomId }, (response: any) => {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
}
});
});
}
// Leave a room
leaveRoom(roomId: string): Promise<any> {
return new Promise((resolve, reject) => {
this.socket.emit('room:leave', { roomId }, (response: any) => {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
}
});
});
}
// Send message to room
sendMessage(roomId: string, content: string): Promise<any> {
return new Promise((resolve, reject) => {
this.socket.emit(
'message:send',
{ roomId, content },
(response: any) => {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
}
}
);
});
}
// Send private message
sendPrivateMessage(recipientId: string, content: string): Promise<any> {
return new Promise((resolve, reject) => {
this.socket.emit(
'message:private',
{ recipientId, content },
(response: any) => {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
}
}
);
});
}
// Typing indicators
startTyping(roomId: string) {
this.socket.emit('typing:start', { roomId });
}
stopTyping(roomId: string) {
this.socket.emit('typing:stop', { roomId });
}
// Event listeners
onMessage(callback: (message: any) => void) {
this.socket.on('message:received', callback);
}
onPrivateMessage(callback: (message: any) => void) {
this.socket.on('message:private', callback);
}
onUserJoined(callback: (data: any) => void) {
this.socket.on('user:joined', callback);
}
onUserLeft(callback: (data: any) => void) {
this.socket.on('user:left', callback);
}
onUserConnected(callback: (data: any) => void) {
this.socket.on('user:connected', callback);
}
onUserDisconnected(callback: (data: any) => void) {
this.socket.on('user:disconnected', callback);
}
onTypingStart(callback: (data: any) => void) {
this.socket.on('typing:start', callback);
}
onTypingStop(callback: (data: any) => void) {
this.socket.on('typing:stop', callback);
}
onUsersList(callback: (users: any[]) => void) {
this.socket.on('users:list', callback);
}
// Disconnect
disconnect() {
this.socket.disconnect();
}
// Get connection status
isConnected(): boolean {
return this.socket.connected;
}
}
// Usage example
async function example() {
const client = new ChatClient({
url: 'http://localhost:3000',
username: 'John Doe',
});
// Listen for messages
client.onMessage((message) => {
console.log(`[${message.username}]: ${message.content}`);
});
// Listen for user events
client.onUserJoined((data) => {
console.log(`${data.username} joined ${data.roomId}`);
});
// Join a room
await client.joinRoom('general');
// Send a message
await client.sendMessage('general', 'Hello, everyone!');
// Typing indicator
client.startTyping('general');
setTimeout(() => client.stopTyping('general'), 2000);
}Build a React chat component:
import { useEffect, useState, useRef } from 'react';
import { ChatClient } from '../chat-client';
interface Message {
id: string;
username: string;
content: string;
timestamp: Date;
}
export function ChatRoom({ roomId, username }: { roomId: string; username: string }) {
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());
const [onlineUsers, setOnlineUsers] = useState<string[]>([]);
const clientRef = useRef<ChatClient | null>(null);
const typingTimeoutRef = useRef<NodeJS.Timeout>();
useEffect(() => {
// Initialize client
const client = new ChatClient({
url: 'http://localhost:3000',
username,
});
clientRef.current = client;
// Setup event listeners
client.onMessage((message) => {
setMessages((prev) => [...prev, message]);
});
client.onUserJoined((data) => {
console.log(`${data.username} joined`);
});
client.onUserLeft((data) => {
console.log(`${data.username} left`);
});
client.onTypingStart((data) => {
setTypingUsers((prev) => new Set(prev).add(data.username));
});
client.onTypingStop((data) => {
setTypingUsers((prev) => {
const next = new Set(prev);
next.delete(data.username);
return next;
});
});
client.onUsersList((users) => {
setOnlineUsers(users.map((u: any) => u.username));
});
// Join room
client.joinRoom(roomId);
// Cleanup
return () => {
client.leaveRoom(roomId);
client.disconnect();
};
}, [roomId, username]);
const handleSendMessage = async () => {
if (!inputValue.trim() || !clientRef.current) return;
await clientRef.current.sendMessage(roomId, inputValue);
setInputValue('');
// Stop typing indicator
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
clientRef.current.stopTyping(roomId);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
if (!clientRef.current) return;
// Start typing indicator
clientRef.current.startTyping(roomId);
// Clear existing timeout
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
// Stop typing after 2 seconds of inactivity
typingTimeoutRef.current = setTimeout(() => {
clientRef.current?.stopTyping(roomId);
}, 2000);
};
return (
<div className="chat-room">
<div className="sidebar">
<h3>Online Users ({onlineUsers.length})</h3>
<ul>
{onlineUsers.map((user) => (
<li key={user}>{user}</li>
))}
</ul>
</div>
<div className="main">
<div className="messages">
{messages.map((message) => (
<div key={message.id} className="message">
<strong>{message.username}:</strong> {message.content}
<span className="timestamp">
{new Date(message.timestamp).toLocaleTimeString()}
</span>
</div>
))}
</div>
{typingUsers.size > 0 && (
<div className="typing-indicator">
{Array.from(typingUsers).join(', ')} {typingUsers.size === 1 ? 'is' : 'are'} typing...
</div>
)}
<div className="input-area">
<input
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Type a message..."
/>
<button onClick={handleSendMessage}>Send</button>
</div>
</div>
</div>
);
}Use namespaces to isolate different applications:
@WebSocketGateway({ namespace: '/chat' })
export class ChatGateway {
// Chat functionality
}
@WebSocketGateway({ namespace: '/notifications' })
export class NotificationsGateway {
// Notification functionality
}
@WebSocketGateway({ namespace: '/admin' })
export class AdminGateway {
// Admin functionality
}Clients connect to specific namespaces:
const chatSocket = io('http://localhost:3000/chat');
const notifSocket = io('http://localhost:3000/notifications');Use rooms to broadcast to specific groups efficiently:
// Join multiple rooms
client.join('room1');
client.join('room2');
client.join(`user:${userId}`); // User-specific room
// Broadcast to specific room
this.server.to('room1').emit('event', data);
// Broadcast to multiple rooms
this.server.to(['room1', 'room2']).emit('event', data);
// Broadcast to all except sender
client.broadcast.emit('event', data);
// Broadcast to room except sender
client.to('room1').broadcast.emit('event', data);Ensure message delivery with acknowledgments:
@SubscribeMessage('message:send')
handleMessage(
@MessageBody() data: any,
@ConnectedSocket() client: Socket,
) {
// Process message
const messageId = this.saveMessage(data);
// Return acknowledgment
return {
success: true,
messageId,
timestamp: new Date(),
};
}Client receives acknowledgment:
socket.emit('message:send', { content: 'Hello' }, (ack) => {
console.log('Message delivered:', ack.messageId);
});Send binary data efficiently:
@SubscribeMessage('file:upload')
handleFileUpload(
@MessageBody() data: { filename: string; buffer: Buffer },
@ConnectedSocket() client: Socket,
) {
// Process binary data
const fileId = this.saveFile(data.filename, data.buffer);
return {
success: true,
fileId,
size: data.buffer.length,
};
}Client sends binary:
const fileBuffer = await file.arrayBuffer();
socket.emit('file:upload', {
filename: file.name,
buffer: Buffer.from(fileBuffer),
});Add middleware for logging, authentication, and rate limiting:
import { Socket } from 'socket.io';
import { Logger } from '@nestjs/common';
export const wsLogger = (socket: Socket, next: (err?: Error) => void) => {
const logger = new Logger('WebSocket');
logger.log(`Client connecting: ${socket.id} from ${socket.handshake.address}`);
// Log all events
socket.onAny((event, ...args) => {
logger.debug(`Event: ${event} from ${socket.id}`);
});
next();
};
export const wsRateLimit = (socket: Socket, next: (err?: Error) => void) => {
const requests = new Map<string, number[]>();
const WINDOW_MS = 60000; // 1 minute
const MAX_REQUESTS = 100;
const clientId = socket.handshake.address;
const now = Date.now();
const clientRequests = requests.get(clientId) || [];
const recentRequests = clientRequests.filter(time => now - time < WINDOW_MS);
if (recentRequests.length >= MAX_REQUESTS) {
return next(new Error('Rate limit exceeded'));
}
recentRequests.push(now);
requests.set(clientId, recentRequests);
next();
};Apply middleware in gateway:
@WebSocketGateway({
namespace: '/chat',
middlewares: [wsLogger, wsRateLimit],
})
export class ChatGateway {
// Gateway code
}Wrong: Assuming connection is always stable
socket.on('connect', () => {
// Only runs on initial connect
loadInitialData();
});Right: Handle reconnection properly
socket.on('connect', () => {
loadInitialData();
});
socket.on('reconnect', () => {
// Resync state after reconnection
resyncData();
rejoinRooms();
});Wrong: Not cleaning up listeners
useEffect(() => {
socket.on('message', handleMessage);
// Missing cleanup!
}, []);Right: Always clean up
useEffect(() => {
socket.on('message', handleMessage);
return () => {
socket.off('message', handleMessage);
};
}, []);Wrong: Sending huge objects
socket.emit('data', {
users: allUsers, // 10,000 users
messages: allMessages, // 100,000 messages
});Right: Paginate and compress
socket.emit('data', {
users: recentUsers.slice(0, 50),
hasMore: true,
});
// Or use compression
socket.emit('data', compressData(largeObject));Wrong: Trusting client data
@SubscribeMessage('message:send')
handleMessage(@MessageBody() data: any) {
// No validation!
this.saveMessage(data);
}Right: Validate everything
@SubscribeMessage('message:send')
handleMessage(@MessageBody() data: any) {
if (!data.content || typeof data.content !== 'string') {
throw new WsException('Invalid message content');
}
if (data.content.length > 1000) {
throw new WsException('Message too long');
}
this.saveMessage(data);
}Wrong: No error handling
@SubscribeMessage('message:send')
async handleMessage(@MessageBody() data: any) {
await this.database.save(data); // Can throw!
}Right: Handle errors gracefully
@SubscribeMessage('message:send')
async handleMessage(@MessageBody() data: any, @ConnectedSocket() client: Socket) {
try {
await this.database.save(data);
return { success: true };
} catch (error) {
this.logger.error('Failed to save message:', error);
client.emit('error', {
message: 'Failed to send message',
code: 'MESSAGE_SAVE_FAILED',
});
return { success: false, error: 'Internal error' };
}
}Detect dead connections:
@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection {
private readonly pingInterval = 30000; // 30 seconds
private readonly pongTimeout = 5000; // 5 seconds
handleConnection(client: Socket) {
let isAlive = true;
const pingTimer = setInterval(() => {
if (!isAlive) {
clearInterval(pingTimer);
client.disconnect();
return;
}
isAlive = false;
client.emit('ping');
setTimeout(() => {
if (!isAlive) {
clearInterval(pingTimer);
client.disconnect();
}
}, this.pongTimeout);
}, this.pingInterval);
client.on('pong', () => {
isAlive = true;
});
client.on('disconnect', () => {
clearInterval(pingTimer);
});
}
}Scale across multiple servers:
npm install @socket.io/redis-adapter redisimport { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
export class RedisIoAdapter extends IoAdapter {
private adapterConstructor: ReturnType<typeof createAdapter>;
async connectToRedis(): Promise<void> {
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
this.adapterConstructor = createAdapter(pubClient, subClient);
}
createIOServer(port: number, options?: ServerOptions): any {
const server = super.createIOServer(port, options);
server.adapter(this.adapterConstructor);
return server;
}
}
// In main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const redisIoAdapter = new RedisIoAdapter(app);
await redisIoAdapter.connectToRedis();
app.useWebSocketAdapter(redisIoAdapter);
await app.listen(3000);
}Handle high message volumes:
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';
import { InjectQueue } from '@nestjs/bull';
@Injectable()
export class MessageQueueService {
constructor(
@InjectQueue('messages') private messageQueue: Queue,
) {}
async queueMessage(message: any) {
await this.messageQueue.add('process', message, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
});
}
}
@Processor('messages')
export class MessageProcessor {
@Process('process')
async handleMessage(job: Job) {
const message = job.data;
// Process message (save to DB, send notifications, etc.)
await this.processMessage(message);
}
}Track WebSocket health:
import { Injectable } from '@nestjs/common';
import { Counter, Gauge, Histogram } from 'prom-client';
@Injectable()
export class WebSocketMetrics {
private readonly connectionsGauge = new Gauge({
name: 'websocket_connections_total',
help: 'Total number of WebSocket connections',
});
private readonly messagesCounter = new Counter({
name: 'websocket_messages_total',
help: 'Total number of messages',
labelNames: ['event', 'status'],
});
private readonly messageLatency = new Histogram({
name: 'websocket_message_duration_seconds',
help: 'Message processing duration',
labelNames: ['event'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
});
incrementConnections() {
this.connectionsGauge.inc();
}
decrementConnections() {
this.connectionsGauge.dec();
}
recordMessage(event: string, status: 'success' | 'error') {
this.messagesCounter.inc({ event, status });
}
recordLatency(event: string, duration: number) {
this.messageLatency.observe({ event }, duration);
}
}Handle server restarts properly:
import { Injectable, OnModuleDestroy } from '@nestjs/common';
@Injectable()
export class ChatGateway implements OnModuleDestroy {
async onModuleDestroy() {
// Notify all clients about shutdown
this.server.emit('server:shutdown', {
message: 'Server is restarting. Please reconnect in a moment.',
reconnectDelay: 5000,
});
// Wait for messages to be sent
await new Promise(resolve => setTimeout(resolve, 1000));
// Close all connections gracefully
this.server.close();
}
}Use WSS (WebSocket Secure) in production:
import { NestFactory } from '@nestjs/core';
import { readFileSync } from 'fs';
import * as https from 'https';
async function bootstrap() {
const httpsOptions = {
key: readFileSync('./secrets/private-key.pem'),
cert: readFileSync('./secrets/certificate.pem'),
};
const app = await NestFactory.create(AppModule, {
httpsOptions,
});
await app.listen(3000);
console.log('Secure WebSocket server running on wss://localhost:3000');
}Despite its strengths, WebSocket is the wrong choice in many scenarios:
If you're building a standard CRUD API, WebSocket is overkill:
Why: REST APIs are simpler, cacheable, and better supported by infrastructure (CDNs, load balancers, proxies).
Alternative: Use REST with HTTP/2 for better performance.
If updates happen rarely (every few minutes or hours), WebSocket wastes resources:
Why: Maintaining persistent connections for infrequent updates is inefficient. Polling or SSE are better.
Alternative: Use HTTP polling with appropriate intervals or Server-Sent Events.
WebSocket isn't designed for large file transfers:
Why: HTTP has better support for range requests, resumable downloads, and CDN caching.
Alternative: Use standard HTTP downloads with progress tracking.
Search engines don't execute WebSocket connections:
Why: Content loaded via WebSocket won't be indexed by search engines.
Alternative: Use server-side rendering with HTTP for initial content, WebSocket for updates.
If your architecture requires stateless services, WebSocket creates challenges:
Why: WebSocket connections are stateful. Load balancing and scaling become complex.
Alternative: Use HTTP APIs or message queues for service-to-service communication.
WebSocket connections drain battery on mobile devices:
Why: Persistent connections prevent the device from entering deep sleep.
Alternative: Use push notifications (FCM/APNS) for mobile apps, WebSocket only when app is active.
Let's build a comprehensive live trading dashboard that demonstrates WebSocket's strengths in a realistic financial scenario.
You're building a cryptocurrency trading platform with:
export interface PriceTick {
symbol: string;
price: number;
volume: number;
change24h: number;
timestamp: Date;
}
export interface OrderBookEntry {
price: number;
amount: number;
total: number;
}
export interface OrderBook {
symbol: string;
bids: OrderBookEntry[];
asks: OrderBookEntry[];
timestamp: Date;
}
export interface Trade {
id: string;
symbol: string;
side: 'buy' | 'sell';
price: number;
amount: number;
userId: string;
status: 'pending' | 'filled' | 'cancelled';
timestamp: Date;
}
export interface Portfolio {
userId: string;
balances: Record<string, number>;
totalValue: number;
change24h: number;
}
export interface Alert {
id: string;
userId: string;
symbol: string;
condition: 'above' | 'below';
targetPrice: number;
triggered: boolean;
}This real-world example demonstrates WebSocket handling high-frequency updates, multiple data streams, and user-specific notifications—exactly what financial platforms need.
WebSocket has revolutionized real-time web applications by providing efficient, bidirectional communication over a single persistent connection. From chat applications to live dashboards, collaborative tools to multiplayer games, WebSocket enables the instant, interactive experiences users expect.
The key insights:
WebSocket excels when you need true real-time, bidirectional communication with minimal latency. Its persistent connection model eliminates polling overhead and enables instant updates in both directions.
WebSocket struggles with simple request-response patterns, infrequent updates, and stateless architectures. For these scenarios, REST, SSE, or polling remain better choices.
The NestJS implementation demonstrates that WebSocket can integrate seamlessly with modern frameworks while maintaining scalability and production-readiness. By understanding WebSocket's strengths and limitations, you can make informed decisions about when it's the right tool for your real-time features.
If you're building applications that require instant updates, user presence, or collaborative features, WebSocket deserves serious consideration. The complexity of managing persistent connections is offset by the superior user experience and efficient resource usage it provides.