The Polling Problem
Before WebSockets, our CAAD ERP ran on polling for "real-time" features. Every 3 seconds, the frontend asked the backend:
- "Any new orders?"
- "Inventory changed?"
- "Print job finished?"
With 50 concurrent users, this meant 600 requests per minute for nothing. Servers buckled. Users saw delays.
Enter WebSockets: The 60x Performance Win
I migrated to WebSockets with these results:
- 60x fewer requests: 10 req/min vs 600 req/min
- Sub-100ms latency: Updates appear instantly
- 50% server load reduction: Less CPU, less DB queries
Architecture Pattern
typescript
[Angular Client] <--> [NestJS Gateway] <--> [Redis Pub/Sub]
↑
[Service A] -----> [Redis Pub/Sub] -----> [Service B]Redis as a message broker lets multiple NestJS instances share state without coupling.
Step 1: WebSocket Gateway
typescript
@WebSocketGateway({
cors: { origin: process.env.FRONTEND_URL },
namespace: 'events'
})
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() server: Server;
private userSockets = new Map<string, Set<Socket>>();
async handleConnection(socket: Socket) {
const userId = socket.handshake.auth.userId;
const tenantId = socket.handshake.auth.tenantId;
if (!userSockets.has(userId)) {
userSockets.set(userId, new Set());
}
userSockets.get(userId).add(socket);
socket.join(`tenant:${tenantId}`);
}
handleDisconnect(socket: Socket) {
const userId = socket.handshake.auth.userId;
userSockets.get(userId)?.delete(socket);
}
@SubscribeMessage('subscribe:orders')
handleSubscribeOrders(socket: Socket, payload: { branchId: string }) {
socket.join(`orders:${payload.branchId}`);
return { success: true };
}
}Step 2: Redis Pub/Sub for Cross-Instance Events
typescript
@Injectable()
export class EventsBroadcaster implements OnModuleInit {
private publisher: Redis;
private subscriber: Redis;
constructor(private gateway: EventsGateway) {}
async onModuleInit() {
this.publisher = new Redis({ host: 'redis', port: 6379 });
this.subscriber = new Redis({ host: 'redis', port: 6379 });
this.subscriber.subscribe('inventory:updated', 'order:created', 'print:complete');
this.subscriber.on('message', (channel, message) => {
const data = JSON.parse(message);
this.broadcast(channel, data);
});
}
private broadcast(channel: string, data: any) {
switch (channel) {
case 'inventory:updated':
this.gateway.server.to(`tenant:${data.tenantId}`).emit('inventory:update', data);
break;
case 'order:created':
this.gateway.server.to(`orders:${data.branchId}`).emit('order:new', data);
break;
case 'print:complete':
this.gateway.server.to(`user:${data.userId}`).emit('print:done', data);
break;
}
}
}Step 3: Angular Service
typescript
@Injectable({ providedIn: 'root' })
export class RealtimeService {
private socket: Socket;
connect(token: string, tenantId: string) {
this.socket = io(`${environment.wsUrl}/events`, {
auth: { token, tenantId },
transports: ['websocket'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000
});
this.socket.on('inventory:update', (data) => {
this.inventoryCache.update(data.productId, data);
});
this.socket.on('order:new', (order) => {
this.notification.show(`New order: ${order.orderNumber}`);
this.orderCache.prepend(order);
});
}
disconnect() {
this.socket?.disconnect();
}
}When NOT to Use WebSockets
WebSockets aren't always the answer:
- One-time data fetches → Use HTTP (cheaper)
- Public data with no auth → Polling or Server-Sent Events
- Mobile with bad networks → SSE with auto-reconnect is simpler
Bottom Line
Polling was killing our servers and frustrating users. WebSockets gave us instant updates and cut infrastructure costs. For multi-user enterprise apps, real-time is no longer optional.