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.
| Concern | Socket.IO (@nestjs/platform-socket.io) | Native ws (@nestjs/platform-ws) |
|---|---|---|
| Protocol | Custom layer on top of WebSocket | Plain WebSocket (RFC 6455) |
| Auto-reconnect | Built in | You implement it |
| Rooms / namespaces | First-class | Not provided |
| Fallback transport | HTTP long-polling | None |
| Acknowledgements | Built in | Manual |
| Client library | Requires socket.io-client | Any standards-compliant client |
| Payload size | Heavier framing | Minimal |
| Best fit | Feature-rich apps, browser clients | Lightweight, 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
wsis non-trivial. Reach forwsonly 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.
| Interface | Method | Fires when |
|---|---|---|
OnGatewayInit | afterInit(server) | The underlying server is ready |
OnGatewayConnection | handleConnection(client, ...args) | A client connects |
OnGatewayDisconnect | handleDisconnect(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.
| Technique | Direction | Connection | Best for |
|---|---|---|---|
| Polling | Client pulls | New request each time | Infrequent updates, simple infra |
| Long-polling | Client pulls (held open) | Reopened per cycle | Legacy fallback, near-real-time |
| SSE | Server pushes only | One long-lived HTTP stream | Notifications, feeds, log tails |
| WebSockets | Bidirectional | One persistent socket | Chat, 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
handleConnectionor 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 overwss://(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.