One command, a real SIGTERM
Your actual start command, a readiness check, real requests in flight, then the signal. Every step is timed.
shutdown-check starts your real service, puts slow requests in flight and sends SIGTERM. Then it verifies those requests finish, readiness is withdrawn, new work is turned away and the process exits before the deadline, without a line of your code changing.
npm install -D shutdown-checkAny Node.js HTTP serverZero dependenciesNode.js 22+ on macOS and LinuxMIT
One service followed through the check: the run in your terminal, the file that sets it up, the drain on a timeline, what a failure tells you, and the gate in CI.
Your actual start command, a readiness check, real requests in flight, then the signal. Every step is timed.
A JSON file names your start command, a readiness route and a slow route. Nothing is imported into your app.
Requests finish, readiness drops to 503, new work is refused and the process exits well before the deadline.
A stable code for the stage that broke, the timeline up to it, the service's own output and the fix.
Exit codes a job can fail on, JSON for scripts, and a JUnit report your CI already knows how to show.
$ npx shutdown-check test PASS SC000: Graceful shutdown verified Timeline: + 5 ms process launched — pid=67911 + 113 ms service ready — HTTP 200 + 113 ms work request sent — #1 GET /slow + 113 ms work request sent — #2 GET /slow + 114 ms work confirmed active — 2 response(s) sent headers; bodies still in progress + 114 ms signal sent — SIGTERM + 115 ms readiness withdrawn — HTTP 503 + 115 ms new request rejected — HTTP 503 + 566 ms work request finished — #1 HTTP 200 + 566 ms work request finished — #2 HTTP 200 + 573 ms process exited — code=0, signal=none + 574 ms shutdown verified — work completed and service exited before deadline $ echo $?0
{ "command": ["node", "dist/server.js"], "baseUrl": "http://127.0.0.1:3000", "readiness": { "path": "/health" }, "workload": { "path": "/slow", "concurrent": 2, "bodyIncludes": "work complete", "started": { "type": "response-headers" } }, "shutdown": { "deadlineMs": 5000, "readinessWithdrawal": true, "newRequests": { "path": "/new", "rejectStatuses": [503] } } }
Two requests in flight, readiness withdrawn and new work rejected during the drain. The config behind the terminal run.
SIGTERM is sent at 114 ms while two requests are in flight. Readiness returns 503 a millisecond later and a new request is rejected with 503. Both requests finish with HTTP 200 at 566 ms, and the process exits with code 0 at 573 ms, well before the 5114 ms deadline.
FAIL SC201: In-flight request was interrupted: aborted + 113 ms signal sent — SIGTERM + 114 ms work request finished — #1 aborted + 114 ms work request finished — #2 aborted + 576 ms process exited — code=0, signal=none
First thing to check
Do not call process.exit(), closeAllConnections() or destroy sockets while requests are still being answered.
- run: npm run build - name: Test graceful shutdown run: npx shutdown-check test --json --junit shutdown-result.xml
Test results · shutdown-result.xml
The job fails on anything but 0