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.
Steps to handle Node.js shutdown signals:
- Create shutdown-service.mjs with the initial HTTP server.
- shutdown-service.mjs
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
- Insert the guarded shutdown() function below the server definition.
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 - Register SIGTERM and SIGINT below the shutdown() function.
process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown);
- Append the HTTP listener startup callback after the signal registrations.
server.listen(port, host, () => { console.log(`listening on http://${host}:${port} pid=${process.pid}`); });
- Compare shutdown-service.mjs with the completed service.
- shutdown-service.mjs
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}`); });
- Check shutdown-service.mjs for JavaScript syntax errors.
$ node --check shutdown-service.mjs
A successful syntax check returns no output and exits with status 0.
- Start the service in the first terminal.
$ node shutdown-service.mjs listening on http://127.0.0.1:3000 pid=2773
- Request the service from a second terminal to confirm it is accepting HTTP work.
$ curl -sS -i http://127.0.0.1:3000/ HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 ##### snipped ##### ready
- Send SIGTERM to the printed process ID from the second terminal.
$ 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.
- Confirm the first terminal reports the signal and completed HTTP close.
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.
- Check the service exit status in the first terminal.
$ echo $? 0
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.