Suggest an editImprove this articleRefine the answer for “How to implement WebSocket Gateways in NestJS?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**WebSocket Gateway in NestJS** is a class with `@WebSocketGateway()` that handles Socket.IO events in real time on a specific namespace. Register it as a provider, subscribe to events with `@SubscribeMessage()`, and broadcast with `@WebSocketServer()`. ```typescript @WebSocketGateway({ namespace: '/chat' }) export class ChatGateway { @WebSocketServer() server: Server; @SubscribeMessage('message') handle(@MessageBody() data: string) { this.server.emit('message', data); // to all clients } } ``` **Key point:** gateways share the HTTP port with controllers but manage persistent connections independently.Shown above the full answer for quick recall.Answer (EN)Image**WebSocket Gateway in NestJS** is a class decorated with `@WebSocketGateway()` that creates a Socket.IO server endpoint for real-time, bidirectional communication - separate from your HTTP routes but sharing the same port. ## Theory ### TL;DR - Gateways work like a hotel front desk for live conversations: HTTP handles one-off check-ins, the gateway keeps an ongoing back-and-forth without a new request each time. - Core difference from controllers: gateways mount a Socket.IO server on a specific namespace; controllers handle REST only. - HTTP and WebSocket share the same port through the Upgrade header mechanism. - Live updates (chat, notifications, presence) - gateway. One-off data fetches - HTTP controller. - For multiple server instances in production, you need a Redis adapter. Without it, broadcasts stay local to one process. ### Quick example Install the packages first: ```bash npm install @nestjs/websockets @nestjs/platform-socket.io socket.io ``` ```typescript import { WebSocketGateway, SubscribeMessage, MessageBody, WebSocketServer } from '@nestjs/websockets'; import { Server } from 'socket.io'; @WebSocketGateway({ namespace: '/chat', cors: { origin: '*' } }) export class ChatGateway { @WebSocketServer() server: Server; @SubscribeMessage('sendMessage') handleMessage(@MessageBody() data: string): void { this.server.emit('newMessage', data); // broadcasts to all connected clients } } // Register as a provider in app.module.ts, not as a controller @Module({ providers: [ChatGateway] }) export class AppModule {} ``` The client connects with `io('http://localhost:3000/chat')` and emits `socket.emit('sendMessage', 'Hello!')`. All connected clients receive the broadcast instantly. ### Key difference from HTTP controllers Controllers use Express/Fastify routers for stateless request-response. Gateways bootstrap a Socket.IO server instance tied to `@WebSocketServer()` and route events through namespaces. The underlying `http.Server` is shared: WebSocket Upgrade requests bypass HTTP routing, hit Socket.IO's handshake, and Engine.IO creates persistent TCP connections. Events route to handlers via decorator metadata. Lifecycle hooks like `handleConnection` fire on `socket.on('connect')`. ### When to use - Typing indicators, user presence, cursor positions - gateway with namespaces. - Load chat history or fetch a user profile - HTTP controller. - More than 10 events per second per user - gateway with rooms. - Stateless API with no real-time requirement - REST, no gateway needed. - Multiple server instances in production - gateway plus a Redis adapter. ### Authentication pattern Pass a JWT in `handshake.auth` from the client. Validate it in `handleConnection` and store the result on `client.data`. For per-method auth, use `@UseGuards()` with a custom `CanActivate` that reads from `context.switchToWs().getClient()`. If validation fails, emit an error and call `client.disconnect()` before any handler runs. ### Scaling with a Redis adapter One NestJS process keeps all socket state in memory. Run two instances and `this.server.to(room).emit()` from one pod only reaches clients on that pod. The Redis adapter syncs broadcasts across instances through pub/sub. Most teams hit this not during local development but the first time they deploy to Kubernetes - after that, the Redis adapter becomes non-negotiable. ### Common mistakes **Wrong namespace in client connect:** ```typescript // Wrong - connects to '/' namespace; no handlers fire io('http://localhost:3000'); // Correct - match the namespace in @WebSocketGateway() io('http://localhost:3000/chat'); ``` The connection appears successful but no events are handled. Logs show "connected", nothing else. **Broadcasting without validating the sender:** ```typescript // Wrong - anyone can push to all clients @SubscribeMessage('msg') handle(@MessageBody() data: any) { this.server.emit('msg', data); } // Correct - check client.data.user first @SubscribeMessage('msg') handle(@MessageBody() data: any, @ConnectedSocket() client: Socket) { if (!client.data.user) return; this.server.emit('msg', { userId: client.data.user.id, ...data }); } ``` **Missing handleDisconnect causes memory leaks:** ```typescript // Wrong - Map grows forever; thousands of ghost entries after a connection spike private users = new Map<string, UserInfo>(); handleConnection(client: Socket) { this.users.set(client.id, {}); } // no handleDisconnect // Correct handleDisconnect(client: Socket) { this.users.delete(client.id); } ``` **`cors: '*'` in production** opens cross-site WebSocket hijacking. Use `{ origin: ['https://myapp.com'], credentials: true }` instead. **Deploying multiple instances without the Redis adapter:** PM2 clusters and Kubernetes pods each keep their own socket state. Broadcasts drop silently for clients on other instances. ### Real-world usage - Chat apps (Rocket.Chat-style): one namespace per feature, rooms per conversation thread. - Live dashboards: price ticks via namespaces, no polling. - Notifications: `@nestjs/bull` job queue plus gateway emit on job completion. - Collaborative tools: cursor position broadcasting across connected clients. - Multiplayer games: player state updates with a room per game session. ### Follow-up questions **Q:** How does NestJS handle WebSocket and HTTP on the same port? **A:** Socket.IO intercepts the HTTP Upgrade header before Express/Fastify routing sees the request. If the handshake passes, Engine.IO keeps the TCP connection open. Normal HTTP requests go through the middleware stack as usual. **Q:** What is the difference between rooms and namespaces? **A:** Namespaces partition the Socket.IO server itself, like separate logical servers on one port. Rooms group clients within a namespace and support dynamic join and leave, but they do not isolate event handlers. **Q:** How do you emit events from a service rather than directly from the gateway? **A:** Inject the gateway into the service and call `gateway.server.to(room).emit(...)`. Or use a shared event emitter the gateway subscribes to. The second approach avoids pulling the gateway dependency into unrelated services. **Q:** What happens to room memberships when a client reconnects? **A:** Socket.IO does not restore memberships automatically. Re-join rooms in `handleConnection` using stored state - a Redis hash or database record keyed by user ID. **Q:** How would you handle sticky sessions for a load-balanced gateway deployment? **A:** Configure Nginx or HAProxy to route by `socket.id` hash so a client always hits the same instance during handshake. Socket.IO v4.7+ also supports `sticky: true` on the server. Without sticky routing, the HTTP handshake and WebSocket upgrade can land on different instances and the connection drops. The Redis adapter is a separate concern: it syncs broadcasts but does not fix handshake routing. ## Examples ### Basic gateway with connection lifecycle ```typescript import { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody, ConnectedSocket, OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; @WebSocketGateway({ namespace: '/chat', cors: { origin: '*' } }) export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; handleConnection(client: Socket) { console.log('Client connected:', client.id); } handleDisconnect(client: Socket) { console.log('Client disconnected:', client.id); } @SubscribeMessage('message') handleMessage( @MessageBody() data: { room: string; text: string }, @ConnectedSocket() client: Socket, ) { this.server.to(data.room).emit('message', { user: client.id, text: data.text, timestamp: new Date(), }); } @SubscribeMessage('joinRoom') handleJoinRoom(@MessageBody() room: string, @ConnectedSocket() client: Socket) { client.join(room); this.server.to(room).emit('userJoined', { userId: client.id }); return { event: 'joinedRoom', data: room }; // acknowledgment back to sender } } ``` `handleConnection` and `handleDisconnect` fire automatically. `@SubscribeMessage('joinRoom')` returns an acknowledgment object directly back to the calling client. ### Chat with rooms and JWT authentication ```typescript import { UseGuards, Injectable } from '@nestjs/common'; import { CanActivate, ExecutionContext } from '@nestjs/common'; @Injectable() export class WsAuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const client = context.switchToWs().getClient<Socket>(); return !!client.data.user; } } @WebSocketGateway({ namespace: '/chat' }) export class ChatGateway implements OnGatewayConnection { constructor(private readonly authService: AuthService) {} @WebSocketServer() server: Server; async handleConnection(client: Socket) { try { const token = client.handshake.auth.token; const user = await this.authService.verifyToken(token); client.data.user = user; // store for later handlers client.join('general'); // auto-join default room } catch { client.emit('error', { message: 'Authentication failed' }); client.disconnect(); // close before any handler runs } } @SubscribeMessage('joinRoom') @UseGuards(WsAuthGuard) joinRoom(@MessageBody() { room }: { room: string }, @ConnectedSocket() client: Socket) { client.join(room); client.emit('joined', `Welcome to ${room}`); } @SubscribeMessage('message') sendToRoom( @MessageBody() { room, msg }: { room: string; msg: string }, @ConnectedSocket() client: Socket, ) { this.server.to(room).emit('message', { userId: client.id, msg }); } } ``` JWT validation happens in `handleConnection` before any message handler runs. `@UseGuards(WsAuthGuard)` adds a second layer on sensitive methods. Only room members receive messages sent with `server.to(room).emit()`. ### Horizontal scaling with Redis adapter ```typescript // main.ts import { NestFactory } from '@nestjs/core'; import { createAdapter } from '@socket.io/redis-adapter'; import { AppModule } from './app.module'; import Redis from 'ioredis'; async function bootstrap() { const app = await NestFactory.create(AppModule); const pubClient = new Redis({ host: 'localhost', port: 6379 }); const subClient = pubClient.duplicate(); // sync broadcasts across all instances via Redis pub/sub app.useWebSocketAdapter(createAdapter(pubClient, subClient)); await app.listen(3000); } bootstrap(); ``` With this setup, `this.server.to('room').emit()` in any gateway reaches clients on all instances. If Redis goes down, instances fall back to in-memory only - meaning pods stop sharing broadcasts but they do not crash.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.