Shutdown Check

Search documentation

Find a page or section

Install the package, connect it to a real route, and run your first shutdown test.

shutdown-check starts your Node.js service, opens a request, sends SIGTERM while that request is active, and watches what happens. A passing result means the request finished, the process exited on time, and the port closed.

Before you begin

You need:

  • Node.js 22 or later;
  • macOS or Linux;
  • a local HTTP/1 service;
  • a readiness route, such as /health;
  • a route that stays active long enough to receive SIGTERM.

The package works with node:http, Express, Fastify, NestJS, and other Node.js frameworks. It does not add shutdown behavior to your app; it tests the behavior you already wrote. See compatibility and limits before using HTTPS, HTTP/2, WebSockets, Windows, or remote services.

1. Install the package

Add shutdown-check as a development dependency:

npm install -D shutdown-check

The package has no runtime dependencies and does not need to ship with your production application.

2. Create the config

Run the initializer from the project root:

npx shutdown-check init

The command creates shutdown-check.json. It will not overwrite an existing file. The generated file is:

shutdown-check.json
{
  "command": ["node", "server.js"],
  "cwd": ".",
  "baseUrl": "http://127.0.0.1:3000",
  "readiness": { "path": "/health", "status": 200, "timeoutMs": 10000 },
  "workload": {
    "path": "/slow",
    "method": "GET",
    "status": 200,
    "started": { "type": "response-headers", "timeoutMs": 5000 }
  },
  "shutdown": { "deadlineMs": 10000, "exitCode": 0 }
}

Update these values:

FieldWhat to enter
commandThe direct command that starts the built service
baseUrlA local port reserved for this test
readiness.pathA route that returns the expected status when the service is ready
workload.pathA route that remains active long enough for the shutdown signal to arrive

Prefer a direct command such as ["node", "dist/server.js"]. A shell or package-manager wrapper may receive the signal instead of the server.

3. Make the workload testable

The starter config uses the response-headers start barrier. The workload route must send headers and keep the response body open. This lets shutdown-check prove that the request is active before it sends SIGTERM.

Here is a complete server that matches the starter config:

server.js
const http = require("node:http");
 
const port = Number(process.env.PORT ?? 3000);
 
const server = http.createServer((request, response) => {
  if (request.url === "/health") {
    response.writeHead(200, { "content-type": "text/plain" });
    response.end("ok");
    return;
  }
 
  if (request.url === "/slow") {
    response.writeHead(200, { "content-type": "text/plain" });
    response.flushHeaders();
    setTimeout(() => response.end("work complete\n"), 2000);
    return;
  }
 
  response.writeHead(404).end();
});
 
server.listen(port, "127.0.0.1", () => {
  console.log(`listening on http://127.0.0.1:${port}`);
});
 
process.on("SIGTERM", () => {
  console.log("SIGTERM received; closing the server");
  server.close((error) => {
    if (error) {
      console.error(error);
      process.exitCode = 1;
    }
  });
});

response.flushHeaders() sends the 200 headers immediately. The body ends two seconds later. When SIGTERM arrives, server.close() stops accepting new connections and waits for the open response to finish.

If your handler sends nothing until its work is complete, use a probe start barrier instead. A normal fast route is not useful: it may finish before the signal, which produces SC111.

4. Run the test

npx shutdown-check test

The test now:

  1. checks that the configured port is free;
  2. starts command in its own process group;
  3. waits for the readiness route;
  4. opens the workload request and confirms that it is active;
  5. sends SIGTERM;
  6. waits for the response and process exit;
  7. checks that the port is closed.

The command exits with 0 after a pass, 1 after a shutdown failure, and 2 when invalid setup or configuration prevents the test from running.

5. Read a passing result

The server above prints a result like this. Timings and process IDs vary.

PASS SC000: Graceful shutdown verified
 
Timeline:
  +    5 ms  process launched — pid=73661
  +  112 ms  service ready — HTTP 200
  +  112 ms  work request sent — #1 GET /slow
  +  113 ms  work confirmed active — 1 response(s) sent headers; bodies still in progress
  +  113 ms  signal sent — SIGTERM
  + 2118 ms  work request finished — #1 HTTP 200
  + 2125 ms  process exited — code=0, signal=none
  + 2126 ms  shutdown verified — work completed and service exited before deadline

The important sequence is:

  • work confirmed active appears before signal sent;
  • the request finishes after the signal;
  • the process exits with code 0;
  • shutdown verified appears last.

SC000 is the pass code. The output reference explains every timeline event.

6. See a real failure

Remove the SIGTERM handler and run the command again. Node.js uses its default signal behavior and exits immediately, cutting off the open request:

FAIL SC201: In-flight request was interrupted: aborted
 
Timeline:
  +    6 ms  process launched — pid=73692
  +  112 ms  service ready — HTTP 200
  +  113 ms  work request sent — #1 GET /slow
  +  113 ms  work confirmed active — 1 response(s) sent headers; bodies still in progress
  +  113 ms  signal sent — SIGTERM
  +  116 ms  process exited — code=null, signal=SIGTERM
  +  116 ms  work request finished — #1 aborted
  +  116 ms  check failed — SC201: In-flight request was interrupted: aborted
 
Service stdout (last 8 KiB):
listening on http://127.0.0.1:3000

The first line gives the stable diagnostic code. Open SC201 for the causes and fix. On failures, the CLI also includes the last 8 KiB of the service's stdout and stderr.

An invalid config looks different. It starts with shutdown-check:, has no SC code or timeline, and exits with 2:

shutdown-check: Cannot read JSON config /path/shutdown-check.json: Error: ENOENT: no such file or directory, open '/path/shutdown-check.json'

Use troubleshooting when the command does not reach a normal test result.

7. Strengthen the check

The starter config covers one request, process exit, and port closure. You can also verify multiple requests, response content, readiness withdrawal, new request rejection, and repeated signals:

shutdown-check.json
{
  "command": ["node", "server.js"],
  "baseUrl": "http://127.0.0.1:3000",
  "readiness": { "path": "/health" },
  "workload": {
    "path": "/slow",
    "bodyIncludes": "work complete",
    "concurrent": 2,
    "started": { "type": "response-headers" }
  },
  "shutdown": {
    "deadlineMs": 10000,
    "readinessWithdrawal": true,
    "newRequests": { "path": "/health" },
    "repeatSignalAfterMs": 500
  }
}

The example server also passes the readiness and new-request checks because server.close() refuses new connections. Applications that remain open while draining can return 503 instead; see readiness and draining.

8. Add the test to CI

Build the service first, then run:

npx shutdown-check test --json --junit shutdown-result.xml

The nonzero exit code fails the job. The JUnit file can be published as a test report, while JSON keeps the complete result in the job log. The CI guide includes GitHub Actions and GitLab examples.