# Troubleshooting

> shutdown-check troubleshooting by symptom: config errors, a busy port, a service that never gets ready, no in-flight work, hung or cut-off requests.

Source: https://shutdown.jscrate.dev/docs/troubleshooting
Last updated: 2026-09-23

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 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](https://shutdown.jscrate.dev/docs/configuration#common-validation-errors)
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:

```json title="shutdown-check.json"
{
  "env": { "PORT": "3510" },
  "baseUrl": "http://127.0.0.1:3510"
}
```

See [SC001](https://shutdown.jscrate.dev/docs/codes/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:

```text
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](https://shutdown.jscrate.dev/docs/in-flight-work) 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](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) 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:

```json
{ "command": ["npm", "start"] }
```

with a direct command:

```json
{ "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](https://shutdown.jscrate.dev/docs/guides/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.

## Related

- [Diagnostic codes](https://shutdown.jscrate.dev/docs/codes)
- [Configuration reference](https://shutdown.jscrate.dev/docs/configuration)
- [Output and reports](https://shutdown.jscrate.dev/docs/output)
- [How the test runs](https://shutdown.jscrate.dev/docs/how-it-works)
- [Tests with node:test and Vitest](https://shutdown.jscrate.dev/docs/guides/test-runners)
