Long-running services often receive a short stop window from a container runtime or service manager before they are terminated. Node.js must turn that signal into an application-level shutdown so in-flight work can finish and the process can return an intentional exit status.
SIGTERM is the normal stop request from many supervisors, while SIGINT is commonly generated by Ctrl+C in a terminal. Registering either listener removes Node.js's default POSIX exit behavior, so the listener must release active resources and allow the event loop to drain.
The sample HTTP service calls server.close() before a bounded force-close timer. This order stops new connections, lets active HTTP requests finish, and still prevents an unresponsive connection from keeping the process alive past the supervisor's grace period.
import http from 'node:http'; import process from 'node:process'; const host = '127.0.0.1'; const port = 3000; let shuttingDown = false; const server = http.createServer((request, response) => { response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }); response.end('ready\n'); });
Related: Create an HTTP server in Node.js
function shutdown(signal) { if (shuttingDown) { return; } shuttingDown = true; console.log(`received ${signal}, closing HTTP server`); const forceCloseTimer = setTimeout(() => { console.error('grace period expired, forcing connections closed'); server.closeAllConnections(); process.exit(1); }, 8000); forceCloseTimer.unref(); server.close((error) => { clearTimeout(forceCloseTimer); if (error) { console.error(error); process.exitCode = 1; return; } console.log('HTTP server closed'); }); }
server.close() begins the graceful stop before the timer can force remaining HTTP connections closed. Upgraded WebSocket or HTTP/2 sessions need their own close logic because server.closeAllConnections() does not close them.
Related: Run CPU work in a Node.js worker thread
process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown);
server.listen(port, host, () => { console.log(`listening on http://${host}:${port} pid=${process.pid}`); });
import http from 'node:http'; import process from 'node:process'; const host = '127.0.0.1'; const port = 3000; let shuttingDown = false; const server = http.createServer((request, response) => { response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }); response.end('ready\n'); }); function shutdown(signal) { if (shuttingDown) { return; } shuttingDown = true; console.log(`received ${signal}, closing HTTP server`); const forceCloseTimer = setTimeout(() => { console.error('grace period expired, forcing connections closed'); server.closeAllConnections(); process.exit(1); }, 8000); forceCloseTimer.unref(); server.close((error) => { clearTimeout(forceCloseTimer); if (error) { console.error(error); process.exitCode = 1; return; } console.log('HTTP server closed'); }); } process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown); server.listen(port, host, () => { console.log(`listening on http://${host}:${port} pid=${process.pid}`); });
$ node --check shutdown-service.mjs
A successful syntax check returns no output and exits with status 0.
$ node shutdown-service.mjs listening on http://127.0.0.1:3000 pid=2773
$ curl -sS -i http://127.0.0.1:3000/ HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 ##### snipped ##### ready
$ kill -TERM 2773
The required process ID is the number printed by the service; 2773 is the observed example. Ctrl+C exercises the same handler through SIGINT.
received SIGTERM, closing HTTP server HTTP server closed
SIGKILL cannot run a JavaScript cleanup handler. A supervisor's hard-kill timeout must exceed the eight-second timer for the graceful path to finish first.
$ echo $? 0