Answer to the core questions
1. Do shutdown hooks close WebSocket connections immediately? No. app.enableShutdownHooks() only guarantees that HTTP request handlers finish. WebSocket connections managed by @nestjs/websockets stay open until the underlying ws server shuts down or the sockets time out. The default idle timeout is 30 s, so clients may see a delayed close frame.
2. How to close them gracefully? Register a beforeShutdown() hook on each gateway (or a global hook) and keep a registry of active sockets. In the hook, call socket.close() and await the close event before allowing the process to exit. This ensures in‑flight messages are sent and the client receives a proper close frame.
3. Documented limitations? The NestJS 9 issue tracker contains #11538, noting that the WebSocket server is not automatically closed by enableShutdownHooks(). The recommendation in the docs is to implement beforeShutdown() or use app.close() in combination with manual socket cleanup.
Practical steps for a typical Express + @nestjs/websockets setup
- Register shutdown hooks
async function bootstrap() {
const app = await NestFactory.create(NestExpressApp);
app.enableShutdownHooks();
await app.listen(3000);
}
- Maintain a socket registry
import { WebSocketGateway, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';
@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
private readonly sockets = new Set();
handleConnection(client: WebSocket) {
this.sockets.add(client);
}
handleDisconnect(client: WebSocket) {
this.sockets.delete(client);
}
beforeShutdown() {
const closePromises = Array.from(this.sockets).map((client) => {
return new Promise((resolve) => {
client.once('close', resolve);
client.close();
});
});
return Promise.all(closePromises);
}
}
- Optional: close the underlying server – if you need to stop accepting new connections immediately, call
server.close() inside beforeShutdown() after the socket registry cleanup.
- Verify shutdown – launch the app, open a WebSocket from a client, then send
SIGTERM. The client should receive a close frame within the timeout you set for beforeShutdown().
Considerations & constraints
- Node’s default shutdown timeout (10 s) may terminate the process before all sockets close. Keep the cleanup short or increase
process.env.NODE_OPTIONS.
- Sending a custom
shutdown frame before closing can prevent message loss.
- If you use clustering or worker threads, ensure the registry is shared or each worker cleans its own sockets.
- Behind a load balancer, you may want to signal the LB to stop routing traffic before initiating the shutdown.
One diagnostic detail that can change the recommendation
Do you instantiate the WebSocket server manually (e.g., via new WebSocket.Server()) or rely solely on the @WebSocketGateway decorator? If you use a custom server, the cleanup logic needs to call server.close() directly, which is not covered by the gateway’s beforeShutdown() hook.