MohammedMohammedMohammedAnas K V

Initializing
0%
Press?for keyboard shortcuts
Back to Blog
Backend

Why I Chose WebSockets Over Polling for Real-Time Enterprise Features

How WebSockets transformed our ERP from sluggish polling to instant real-time updates, with practical patterns for notifications, inventory sync, and live dashboards.

February 25, 2025
11 min read
WebSocketNestJSAngularReal-timeArchitecture

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.

Technologies

WebSocketNestJSAngularReal-timeArchitecture
Share