Shutdown Check

Search documentation

Find a page or section

Match the first error and last timeline event to the part of the service that needs attention.

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

  1. Read the diagnostic code or setup message.
  2. Find the last successful event before check failed.
  3. Read Service stderr and Service stdout.
  4. Fix the earliest failure first.
  5. 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 patternWhat it meansFix
Cannot read JSON config … ENOENTThe file path is wrongRun from the right folder or pass --config
Cannot read JSON config … SyntaxErrorThe file is not valid JSONRemove comments, trailing commas, or invalid quotes
Unknown command "run"The command name is unsupportedUse init or test
Unknown option "--verbose"The flag is unsupportedCheck shutdown-check --help
--config requires a file pathThe flag has no valueAdd the config path
--junit requires a file pathThe flag has no valueAdd the report path
EEXIST: file already existsinit will not overwriteEdit, rename, or choose another file
command must be a non-empty array of stringscommand is missing or written as one stringUse ["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.0Use a supported local HTTP origin
readiness.path must be a local path beginning with one /A route is missing / or is a full URLUse /health
workload.concurrent above 1 requires the response-headers start barrier…A probe cannot track multiple requestsUse one request or response-headers
shutdown.newRequests requires shutdown.readinessWithdrawal: true…Rejection cannot be timed before a drainEnable readiness withdrawal
shutdown.repeatSignalAfterMs must be shorter than shutdown.deadlineMsThe second signal would arrive too lateLower 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:

shutdown-check.json
{
  "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;
  • command or cwd points 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/health

Then confirm:

  • the application listens on that host and port;
  • the route returns readiness.status, usually 200;
  • 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 concurrent is greater than one.

For probe:

  • return inactiveStatus before the request;
  • return activeStatus only 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=SIGTERM usually 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=SIGTERM means 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:

  1. Give each parallel job a separate port.
  2. Build the service before running the test.
  3. Use isolated database and queue resources.
  4. Make the workload clearly longer than the start-barrier and signal delay.
  5. Set the test-runner timeout above readiness, barrier, and shutdown timeouts.
  6. 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.