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-checkThe 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 initThe command creates shutdown-check.json. It will not overwrite an existing
file. The generated file is:
{
"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:
| Field | What to enter |
|---|---|
command | The direct command that starts the built service |
baseUrl | A local port reserved for this test |
readiness.path | A route that returns the expected status when the service is ready |
workload.path | A 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:
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 testThe test now:
- checks that the configured port is free;
- starts
commandin its own process group; - waits for the readiness route;
- opens the workload request and confirms that it is active;
- sends
SIGTERM; - waits for the response and process exit;
- 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 deadlineThe important sequence is:
work confirmed activeappears beforesignal sent;- the request finishes after the signal;
- the process exits with code
0; shutdown verifiedappears 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:3000The 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:
{
"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.xmlThe 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.