Graceful Shutdown Hooks in NestJS 9 and Pending WebSocket Connection Handling
0 reputation · 22 Feb 2020, 02:28 UTC
0 reputation · 22 Feb 2020, 02:28 UTC
Goal: Achieve zero‑downtime migration of a small NestJS 9 application that uses the Express adapter and @nestjs/websockets by enabling graceful shutdown hooks so that in‑flight HTTP requests finish before the process exits.
Uncertainty: While app.enableShutdownHooks() guarantees that HTTP handlers complete, it is unclear whether the same mechanism reliably terminates active WebSocket connections managed by the @nestjs/websockets package, or whether those sockets remain open until their idle timeout expires.
Specific 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.
async function bootstrap() {
const app = await NestFactory.create(NestExpressApp);
app.enableShutdownHooks();
await app.listen(3000);
}
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);
}
}
server.close() inside beforeShutdown() after the socket registry cleanup.
SIGTERM. The client should receive a close frame within the timeout you set for beforeShutdown().
process.env.NODE_OPTIONS.
shutdown frame before closing can prevent message loss.
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.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 22 Feb 2020, 11:11 UTC
One critical detail to consider when implementing manual socket cleanup is the distinction between closing individual sockets and closing the underlying WebSocket server. While iterating through a registry of sockets to call socket.close() works, it does not stop the server from accepting new connections during the shutdown window.
For a more robust shutdown in NestJS 9, you can inject the server instance directly into your gateway and call the server's close method within the beforeApplicationShutdown hook. This prevents new connections while the existing ones are being drained:
@WebSocketServer() server: Server;
async onApplicationShutdown() {
// Stop accepting new connections and close existing ones
await this.server.close();
}
Note that behavior may vary depending on whether you use the ws or socket.io adapter. When using socket.io, server.close() handles the disconnection of all connected clients automatically, whereas ws may require a manual loop to ensure all sockets receive the close frame before the process terminates.