Skip to content
NestJS ns websockets 4 min read

WebSockets Overview

HTTP is a request/response protocol: the client asks, the server answers, and the connection is done. Real-time features — chat, live dashboards, multiplayer state, collaborative editing — need the server to push data to clients whenever something changes, without waiting to be asked. WebSockets provide a single, long-lived, bidirectional TCP connection for exactly this, and NestJS exposes them through gateways: classes that look and feel like controllers but speak events instead of routes.

What a gateway is

A gateway is any class decorated with @WebSocketGateway(). NestJS wires it into the same dependency injection container as the rest of your app, so you can inject services, repositories, and config exactly as you would in a controller. Instead of mapping HTTP verbs to handler methods, you map message events with @SubscribeMessage().

Gateways are transport-agnostic. By default NestJS uses the Socket.IO adapter (@nestjs/platform-socket.io), but you can swap in the lighter, spec-compliant ws adapter without changing your gateway logic.

import {
  WebSocketGateway,
  SubscribeMessage,
  MessageBody,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server } from 'socket.io';

@WebSocketGateway({ cors: { origin: '*' } })
export class EventsGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('ping')
  handlePing(@MessageBody() data: string): string {
    return `pong: ${data}`; // returned value is emitted back to the sender
  }

  @SubscribeMessage('broadcast')
  handleBroadcast(@MessageBody() message: string): void {
    this.server.emit('message', message); // push to every connected client
  }
}

Register the gateway as a provider, just like a service:

import { Module } from '@nestjs/common';
import { EventsGateway } from './events.gateway';

@Module({
  providers: [EventsGateway],
})
export class EventsModule {}

Socket.IO vs native ws

Both adapters ship as official packages. The right choice depends on how much you value built-in features versus raw protocol fidelity.

ConcernSocket.IO (@nestjs/platform-socket.io)Native ws (@nestjs/platform-ws)
ProtocolCustom layer on top of WebSocketPlain WebSocket (RFC 6455)
Auto-reconnectBuilt inYou implement it
Rooms / namespacesFirst-classNot provided
Fallback transportHTTP long-pollingNone
AcknowledgementsBuilt inManual
Client libraryRequires socket.io-clientAny standards-compliant client
Payload sizeHeavier framingMinimal
Best fitFeature-rich apps, browser clientsLightweight, interop, IoT

To switch to the native ws adapter, install @nestjs/platform-ws and ws, then register the adapter in main.ts:

import { NestFactory } from '@nestjs/core';
import { WsAdapter } from '@nestjs/platform-ws';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useWebSocketAdapter(new WsAdapter(app));
  await app.listen(3000);
}
bootstrap();

Tip: If you need rooms, namespaces, or automatic reconnection, stay on Socket.IO — re-creating those on raw ws is non-trivial. Reach for ws only when you need a minimal footprint or must interoperate with non-Socket.IO clients.

The gateway lifecycle

NestJS gives gateways three optional lifecycle hooks. Implement the matching interface and Nest calls the method automatically — ideal for tracking connections, authenticating on connect, or cleaning up state.

InterfaceMethodFires when
OnGatewayInitafterInit(server)The underlying server is ready
OnGatewayConnectionhandleConnection(client, ...args)A client connects
OnGatewayDisconnecthandleDisconnect(client)A client disconnects
import {
  WebSocketGateway,
  OnGatewayInit,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Logger } from '@nestjs/common';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({ cors: { origin: '*' } })
export class EventsGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
  private readonly logger = new Logger(EventsGateway.name);

  afterInit(server: Server) {
    this.logger.log('WebSocket server initialized');
  }

  handleConnection(client: Socket) {
    this.logger.log(`Client connected: ${client.id}`);
  }

  handleDisconnect(client: Socket) {
    this.logger.log(`Client disconnected: ${client.id}`);
  }
}

Output:

[Nest] LOG [EventsGateway] WebSocket server initialized
[Nest] LOG [EventsGateway] Client connected: q7Xz1aB3kLmN0pQrAAAB
[Nest] LOG [EventsGateway] Client disconnected: q7Xz1aB3kLmN0pQrAAAB

WebSockets vs SSE vs polling

WebSockets are powerful but not always the right tool. Match the transport to the communication pattern.

TechniqueDirectionConnectionBest for
PollingClient pullsNew request each timeInfrequent updates, simple infra
Long-pollingClient pulls (held open)Reopened per cycleLegacy fallback, near-real-time
SSEServer pushes onlyOne long-lived HTTP streamNotifications, feeds, log tails
WebSocketsBidirectionalOne persistent socketChat, games, collaboration

If data only flows from the server (a price ticker, a notification feed), Server-Sent Events are simpler: they ride on plain HTTP, reconnect automatically, and need no special adapter. NestJS supports SSE natively via the @Sse() decorator. Choose WebSockets when the client must also send frequent, low-latency messages back — anything genuinely interactive.

Best practices

  • Keep gateways thin: delegate domain logic to injected services and treat the gateway as a transport boundary, exactly like a controller.
  • Validate every inbound payload with pipes and DTOs — clients are untrusted, and a socket message is as risky as an HTTP body.
  • Authenticate on connection (in handleConnection or a guard) rather than per message, and reject unauthenticated sockets early.
  • Prefer Socket.IO rooms/namespaces over manually tracking client IDs when you need targeted broadcasts.
  • Set explicit CORS origins in production instead of '*', and serve over wss:// (TLS) so tokens and payloads are encrypted.
  • Plan for horizontal scaling from the start: a single-node in-memory server cannot broadcast across instances without a shared adapter such as the Redis adapter.
Last updated June 14, 2026
Was this helpful?