How to handle shutdown signals in a Node.js service

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:

  1. 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');
    });
  2. 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

  3. Register SIGTERM and SIGINT below the shutdown() function.
    process.on('SIGTERM', shutdown);
    process.on('SIGINT', shutdown);
  4. Append the HTTP listener startup callback after the signal registrations.
    server.listen(port, host, () => {
      console.log(`listening on http://${host}:${port} pid=${process.pid}`);
    });
  5. 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}`);
    });
  6. 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.

  7. Start the service in the first terminal.
    $ node shutdown-service.mjs
    listening on http://127.0.0.1:3000 pid=2773
  8. 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
  9. 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.

  10. 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.

  11. Check the service exit status in the first terminal.
    $ echo $?
    0