Start with the first output line. A FAIL SC… result means the test ran and
found a shutdown problem. A line beginning with shutdown-check: means the
test could not start because an argument, file, or config value is invalid.
Use this debugging order
- Read the diagnostic code or setup message.
- Find the last successful event before
check failed. - Read
Service stderrandService stdout. - Fix the earliest failure first.
- Run the same command again before changing another setting.
Later stages may also be broken, but shutdown-check reports the first useful failure. One change at a time keeps the result clear.
The CLI exits with code 2
Exit code 2 means no shutdown test ran. Fix the message printed after
shutdown-check:.
| Message or pattern | What it means | Fix |
|---|---|---|
Cannot read JSON config … ENOENT | The file path is wrong | Run from the right folder or pass --config |
Cannot read JSON config … SyntaxError | The file is not valid JSON | Remove comments, trailing commas, or invalid quotes |
Unknown command "run" | The command name is unsupported | Use init or test |
Unknown option "--verbose" | The flag is unsupported | Check shutdown-check --help |
--config requires a file path | The flag has no value | Add the config path |
--junit requires a file path | The flag has no value | Add the report path |
EEXIST: file already exists | init will not overwrite | Edit, rename, or choose another file |
command must be a non-empty array of strings | command is missing or written as one string | Use ["node", "dist/server.js"] |
baseUrl must use http://localhost, http://127.0.0.1 or http://[::1] | The URL is remote, HTTPS, or 0.0.0.0 | Use a supported local HTTP origin |
readiness.path must be a local path beginning with one / | A route is missing / or is a full URL | Use /health |
workload.concurrent above 1 requires the response-headers start barrier… | A probe cannot track multiple requests | Use one request or response-headers |
shutdown.newRequests requires shutdown.readinessWithdrawal: true… | Rejection cannot be timed before a drain | Enable readiness withdrawal |
shutdown.repeatSignalAfterMs must be shorter than shutdown.deadlineMs | The second signal would arrive too late | Lower the repeat delay |
Validation reports one field at a time. Fix the displayed value and run again. The configuration reference lists the supported shapes and ranges.
The port is already in use
SC001 appears before the service is launched. Another process answered on
baseUrl, or the port could not be confirmed free.
Check for:
- a development server left running;
- another test using the same port;
- parallel CI jobs sharing one config;
- a service container or proxy on that port;
- a child process left by a previous wrapper command.
Give the test a dedicated port and pass it to the service:
{
"env": { "PORT": "3510" },
"baseUrl": "http://127.0.0.1:3510"
}See SC001 when the owner of the port is unclear.
The service exits before readiness
SC101 means the launched process crashed, ended, or could not start.
Read Service stderr first. Typical causes are:
- the build output does not exist;
commandorcwdpoints to the wrong location;- a required environment variable is missing;
- the service has a syntax or import error;
- the service tries to bind another occupied port;
- a launcher exits after starting the real server in the background.
Run the exact command from the configured cwd outside shutdown-check. When it
stays running and listens on the correct port, rerun the test.
The service never becomes ready
SC100 means the process stayed alive but the readiness route never returned
the expected status.
Check the full URL:
baseUrl + readiness.path
http://127.0.0.1:3510 + /health
http://127.0.0.1:3510/healthThen confirm:
- the application listens on that host and port;
- the route returns
readiness.status, usually200; - authentication does not protect the route;
- startup can finish inside
readiness.timeoutMs; - the service is not listening only on a Unix socket.
Raise the timeout only after verifying that startup is genuinely slow.
The work was never in flight
SC111 means the request ended before shutdown-check could prove it was
active, or the barrier never reached its active state.
For response-headers:
- send headers before the work completes;
- call
response.flushHeaders()or write an early body chunk; - keep the response body open long enough for
SIGTERM; - use the same barrier when
concurrentis greater than one.
For probe:
- return
inactiveStatusbefore the request; - return
activeStatusonly while the operation runs; - keep the workload response open until the probe changes;
- use one workload request.
Do not use a fast health endpoint as the workload. The start-barrier guide has complete examples for both patterns.
The request is interrupted
SC201 means the connection closed before the active response finished. Look
at the process exited line:
code=null, signal=SIGTERMusually means no effective signal handler;- a normal exit immediately after the signal often means
process.exit()was called too early; - no process exit may point to code that destroys sockets during shutdown.
Use server.close() and wait for active requests. Do not destroy active
sockets or force the process to exit. The
Node.js shutdown guide provides
node:http, Express, and Fastify handlers.
The request never finishes
SC200 means the response stayed open until shutdown.deadlineMs.
Add logs around:
- the workload handler;
- database and upstream calls;
- locks and queues;
- the start and end of each shutdown step;
- the
server.close()callback.
Look for circular waits. A common example is closing a database pool before an active request has finished using it. Add operation timeouts and close shared resources after HTTP work drains.
Only increase the deadline when the operation is expected to take longer and the new value still fits within the platform's shutdown limit.
The response status or body is wrong
SC202 means the final status differs from workload.status. SC203 means
the status matched but the body does not contain workload.bodyIncludes.
Run the workload normally and record the successful response. Then check that:
- shutdown middleware rejects only new requests, not active ones;
- the configured expected status is correct;
- the body marker is stable and case-sensitive;
- the route does not return an error page with status
200; - the body marker appears within the first 1 MiB.
Use a marker that proves the work completed, such as "export complete", not
a timestamp or generated ID.
The process does not exit
SC300 means requests finished but Node.js still has an open handle.
Common handles include:
- the HTTP server;
- database pools;
- queue consumers;
- intervals and timers;
- file watchers;
- background workers;
- a wrapper process.
Log each cleanup step. Close resources, clear intervals, and add timeouts to
cleanup that can block. Let Node.js exit when the event loop is empty instead
of calling process.exit().
The process exits with the wrong code
SC301 compares the real exit with shutdown.exitCode.
- A nonzero code usually comes from failed cleanup or
process.exitCode. code=null, signal=SIGTERMmeans the signal itself ended the process.- Another signal may indicate a crash or external kill.
Inspect stderr for the original error. Keep the expected code at 0 unless a
different code is an intentional part of your service contract.
The process exits but the port stays open
SC302 usually means the launched command was a wrapper and the real server is
still running as its child.
Replace:
{ "command": ["npm", "start"] }with a direct command:
{ "command": ["node", "dist/server.js"] }If a wrapper is required, it must forward SIGTERM and wait for its child.
Container commands should use exec form rather than shell form.
Readiness or new-request checks fail
SC310 means readiness stayed ready. Set a draining flag immediately after
SIGTERM and make the readiness route return 503, or close the listener.
SC311 means a new request was accepted or timed out after readiness changed.
Return one of newRequests.rejectStatuses, usually 503, or refuse the
connection when that behavior is allowed.
SC312 means the original workload ended before the new request could be
tested. Make the workload longer or withdraw readiness sooner.
See readiness and draining for a complete server and passing timeline.
The result is SC999
SC999 is an unexpected internal or operating-system error. Read the original
message. If you call runCheck() directly, validate the config with
parseConfig() first.
If a valid config repeatedly returns SC999, report it with:
- shutdown-check and Node.js versions;
- operating system;
- config with secrets removed;
- full result message and timeline;
- a small reproduction when possible.
The test is flaky in CI
Check these before increasing every timeout:
- Give each parallel job a separate port.
- Build the service before running the test.
- Use isolated database and queue resources.
- Make the workload clearly longer than the start-barrier and signal delay.
- Set the test-runner timeout above readiness, barrier, and shutdown timeouts.
- Avoid depending on fixed millisecond ordering between unrelated processes.
Use the timeline from both passing and failing runs to identify the stage whose duration changes.