# Configuration

> Every shutdown-check config field in shutdown-check.json: command, baseUrl, readiness, workload, start barrier and shutdown, with defaults, ranges and errors.

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

The shutdown-check config is a JSON file named `shutdown-check.json`. It tells
the CLI how to start your service, wait for readiness, create active work,
send `SIGTERM`, and decide whether the shutdown passed.

## Create the starter file

Run:

```bash
npx shutdown-check init
```

The command writes this complete config and refuses to overwrite an existing
file:

```json title="shutdown-check.json"
{
  "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 }
}
```

Before the first run, change the command, port, readiness path, and workload
path. The [quick start](https://shutdown.jscrate.dev/docs/quick-start) includes a server that matches this
file.

## Configuration rules

- The CLI accepts JSON only. Comments and trailing commas are invalid.
- Every numeric value must be an integer inside its documented range.
- Validation stops at the first invalid value.
- Invalid config prints `shutdown-check: <message>` and exits with code `2`.
- Unknown fields are ignored. A misspelled optional field therefore falls back
  to its default instead of raising an error.
- Route paths must stay on `baseUrl`; full URLs and `//host/path` are rejected.

## All fields

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `command` (required) | `string[]` | — | How to start the service, as an argument array rather than a shell string: `["node", "dist/server.js"]`. It is spawned directly, in its own process group. |
| `cwd` | `string` | the config file's folder | Working directory for `command`, resolved relative to the config file (or to `baseDirectory` in the API). |
| `env` | `Record<string, string>` | `{}` | Environment variables added to, or overriding, the check's own environment for the launched process. |
| `baseUrl` (required) | `string` | — | The service's origin. Must be plain HTTP on `localhost`, `127.0.0.1` or `[::1]`, with no path, query or credentials. Use a port reserved for the test. |
| `readiness` (required) | `object` | — | The route that says the service is ready to take traffic. Polled until it returns `status`. |
| `readiness.path` (required) | `string` | — | Local path of the readiness route, such as `/health`. |
| `readiness.status` | `number` | `200` | The status that means ready. 100–599. |
| `readiness.timeoutMs` | `number` | `10000` | How long to wait for the service to become ready. 100–300000 ms. |
| `readiness.intervalMs` | `number` | `100` | Time between readiness polls. 10–10000 ms. |
| `workload` (required) | `object` | — | The slow request that is in flight when SIGTERM arrives, and what its response must look like. |
| `workload.path` (required) | `string` | — | Local path of an endpoint that takes long enough to still be running when the signal is sent. |
| `workload.method` | `string` | `"GET"` | HTTP method, upper-cased. |
| `workload.headers` | `Record<string, string>` | `{}` | Request headers, as strings. |
| `workload.body` | `string` | — | Request body, as a string. |
| `workload.status` | `number` | `200` | The status every in-flight request must finish with. Anything else is SC202. |
| `workload.bodyIncludes` | `string` | — | Text every in-flight response body must contain, to prove the work really completed. Missing text is SC203. |
| `workload.concurrent` | `number` | `1` | How many workload requests to have in flight at once. 1–20; above 1 needs the `response-headers` barrier. |
| `workload.started` (required) | `object` | — | How the check knows the work has really started before it sends SIGTERM: a start barrier. See [start barriers](https://shutdown.jscrate.dev/docs/in-flight-work). |
| `workload.started.type` (required) | `"response-headers" \| "probe"` | — | `"response-headers"`: work counts as started once the response headers arrive and the body is still open. `"probe"`: a separate route reports it. |
| `workload.started.timeoutMs` | `number` | `5000` | How long to wait for the work to start. 100–300000 ms. Not starting in time is SC111. |
| `workload.started.path` | `string` | — | With `type: "probe"` (required there): the route that reports whether work is running. |
| `workload.started.inactiveStatus` | `number` | `204` | With `type: "probe"`: the status the probe returns while no work is running. |
| `workload.started.activeStatus` | `number` | `200` | With `type: "probe"`: the status it returns while work is running. Must differ from `inactiveStatus`. |
| `workload.started.intervalMs` | `number` | `50` | With `type: "probe"`: time between probe polls. 10–10000 ms. |
| `shutdown` | `object` | — | What a graceful shutdown must look like once SIGTERM is sent. |
| `shutdown.signal` | `"SIGTERM"` | `"SIGTERM"` | The signal sent. Version 1.0 supports only `SIGTERM`. |
| `shutdown.deadlineMs` | `number` | `10000` | How long, from the signal, the work has to finish and the process has to exit. 100–300000 ms. |
| `shutdown.exitCode` | `number` | `0` | The exit code the process must exit with. 0–255. |
| `shutdown.readinessWithdrawal` | `boolean` | `false` | Require the readiness route to stop returning its ready status (or refuse connections) after SIGTERM, before the deadline. |
| `shutdown.newRequests` | `object` | — | Send one new GET during the drain and require it to be rejected. Needs `readinessWithdrawal: true`. |
| `shutdown.newRequests.path` (required) | `string` | — | A safe, test-only route to send the new request to while old work drains. |
| `shutdown.newRequests.rejectStatuses` | `number[]` | `[503]` | Statuses that count as rejecting the new request. |
| `shutdown.newRequests.allowConnectionRefused` | `boolean` | `true` | Whether a refused connection also counts as a rejection. A timeout never does. |
| `shutdown.repeatSignalAfterMs` | `number` | — | Send SIGTERM a second time this long after the first, while work is still running, to check a repeated signal does not cut it off. 10–300000 ms, and less than `deadlineMs`. |

The generated table above contains every type and default. The sections below
explain how the fields work together and what to choose in a real service.

## Command, cwd, and env

```json title="shutdown-check.json"
{
  "command": ["node", "dist/server.js"],
  "cwd": ".",
  "env": {
    "PORT": "3510",
    "NODE_ENV": "production"
  }
}
```

### `command`

Write the executable and each argument as separate array items. shutdown-check
does not use a shell.

Prefer the real server process:

```json
{ "command": ["node", "dist/server.js"] }
```

Avoid a shell string or wrapper when possible:

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

A wrapper may receive `SIGTERM` without forwarding it. It may also exit while
its child server keeps the port open, which produces
[SC302](https://shutdown.jscrate.dev/docs/codes/sc302).

### `cwd`

`cwd` is resolved from the folder containing the config file. If it is
omitted, the command runs in that folder.

For `config/shutdown-check.json`, this starts the command from the project
root:

```json
{ "cwd": ".." }
```

### `env`

`env` adds to or overrides the environment inherited by the CLI. All values
must be strings. Use it to give each test a dedicated port or enable test-only
routes.

```json
{
  "env": {
    "PORT": "3510",
    "ENABLE_TEST_ROUTES": "1"
  }
}
```

## baseUrl

`baseUrl` is the origin used for readiness, workload, probe, and new-request
paths. It must be plain HTTP on the local machine.

| Accepted                 | Rejected                    |
| ------------------------ | --------------------------- |
| `http://127.0.0.1:3510`  | `https://127.0.0.1:3510`    |
| `http://localhost:3510`  | `http://example.com:3510`   |
| `http://[::1]:3510`      | `http://0.0.0.0:3510`       |
| `http://127.0.0.1:3510/` | `http://127.0.0.1:3510/api` |

Reserve the port for one test. shutdown-check requires it to be free before
launch and closed after exit. Parallel tests must use different ports.

## Readiness

Readiness tells shutdown-check when startup has completed.

```json
{
  "readiness": {
    "path": "/health",
    "status": 200,
    "timeoutMs": 30000,
    "intervalMs": 100
  }
}
```

| Field        | Default  | Allowed value                 |
| ------------ | -------- | ----------------------------- |
| `path`       | required | Local path beginning with `/` |
| `status`     | `200`    | HTTP status from 100 to 599   |
| `timeoutMs`  | `10000`  | 100 to 300000                 |
| `intervalMs` | `100`    | 10 to 10000                   |

The tool polls until the route returns exactly `status`. A refused connection
or another status is retried. If the process stays alive without becoming
ready, the result is [SC100](https://shutdown.jscrate.dev/docs/codes/sc100). If it exits first, the result
is [SC101](https://shutdown.jscrate.dev/docs/codes/sc101).

The same route is used after `SIGTERM` when readiness withdrawal is enabled.

## Workload

The workload is the request that must be active when `SIGTERM` arrives.

```json
{
  "workload": {
    "path": "/reports/export",
    "method": "POST",
    "headers": { "content-type": "application/json" },
    "body": "{\"rows\":5000}",
    "status": 200,
    "bodyIncludes": "export complete",
    "concurrent": 3,
    "started": {
      "type": "response-headers",
      "timeoutMs": 5000
    }
  }
}
```

| Field          | Default  | Purpose                                      |
| -------------- | -------- | -------------------------------------------- |
| `path`         | required | Local route used for the work                |
| `method`       | `GET`    | HTTP method                                  |
| `headers`      | `{}`     | Request headers with string values           |
| `body`         | none     | Raw request body                             |
| `status`       | `200`    | Expected final response status               |
| `bodyIncludes` | none     | Case-sensitive text required in the response |
| `concurrent`   | `1`      | Number of requests, from 1 to 20             |
| `started`      | required | Rule that proves the workload is active      |

Choose a safe route that takes long enough to receive the signal. Use
`bodyIncludes` when a successful status alone does not prove that the work
completed. shutdown-check captures up to 1 MiB of each response body.

After `SIGTERM`, each request must finish before the deadline. A timeout,
interruption, wrong status, or wrong body produces `SC200` through `SC203`.

## Start barrier

The start barrier prevents a false pass with work that ended before the
signal. Choose one of two types.

### response-headers

```json
{
  "started": {
    "type": "response-headers",
    "timeoutMs": 5000
  }
}
```

Every workload request must receive headers while its body remains open. This
barrier supports 1 to 20 concurrent requests. The test route normally calls
`response.flushHeaders()` and finishes the body later.

### probe

```json
{
  "started": {
    "type": "probe",
    "path": "/test/work-active",
    "inactiveStatus": 204,
    "activeStatus": 200,
    "timeoutMs": 5000,
    "intervalMs": 50
  }
}
```

Before the workload, the probe must return `inactiveStatus`. While the request
is still open, it must change to `activeStatus`. The statuses must differ.

A probe supports exactly one workload request. Use it for handlers that do not
send response headers until their work is complete. Keep the route test-only
if it exposes internal application state.

See [in-flight work and start barriers](https://shutdown.jscrate.dev/docs/in-flight-work) for complete
server examples and failure output.

## Shutdown

The `shutdown` object defines the contract after the first `SIGTERM`.

```json
{
  "shutdown": {
    "signal": "SIGTERM",
    "deadlineMs": 15000,
    "exitCode": 0,
    "readinessWithdrawal": true,
    "newRequests": {
      "path": "/test/new-work",
      "rejectStatuses": [503],
      "allowConnectionRefused": true
    },
    "repeatSignalAfterMs": 500
  }
}
```

### `signal`

Only `SIGTERM` is supported. Omit the field unless you want the config to state
the signal explicitly.

### `deadlineMs`

The default is 10000 ms. The timer begins with the first signal and covers
active responses, traffic-draining checks, and process exit. Set it above the
slowest valid request and cleanup time, but below the platform's termination
deadline.

### `exitCode`

The default is `0`. The process must exit with this exact code. A process ended
by a signal has no exit code and does not match.

### `readinessWithdrawal`

When `true`, the readiness route must stop returning `readiness.status` before
the deadline. Another status or a refused connection passes. A timeout does
not.

### `newRequests`

This check requires `readinessWithdrawal: true`. After readiness changes and
while the original workload is still active, shutdown-check sends one `GET` to
`newRequests.path`.

The request passes when it returns a status in `rejectStatuses`, or when the
connection is refused and `allowConnectionRefused` is `true`. A timeout or a
normal success response fails.

Use a safe test route because the request is real. The
[readiness and draining guide](https://shutdown.jscrate.dev/docs/guides/readiness-and-draining) shows both
the `503` and closed-listener patterns.

### `repeatSignalAfterMs`

When set, shutdown-check sends a second `SIGTERM` while work is still active.
The value must be shorter than `deadlineMs` and the test workload duration.
Use it to catch handlers that force an early exit after a repeated signal.

## A complete strict config

This example checks three requests, response content, readiness withdrawal,
new-request rejection, a repeated signal, process exit, and port closure:

```json title="shutdown-check.json"
{
  "command": ["node", "dist/server.js"],
  "cwd": ".",
  "env": { "PORT": "3510", "NODE_ENV": "production" },
  "baseUrl": "http://127.0.0.1:3510",
  "readiness": { "path": "/health", "status": 200, "timeoutMs": 30000 },
  "workload": {
    "path": "/slow",
    "status": 200,
    "bodyIncludes": "work complete",
    "concurrent": 3,
    "started": { "type": "response-headers", "timeoutMs": 5000 }
  },
  "shutdown": {
    "deadlineMs": 15000,
    "exitCode": 0,
    "readinessWithdrawal": true,
    "newRequests": {
      "path": "/test/new-work",
      "rejectStatuses": [503],
      "allowConnectionRefused": true
    },
    "repeatSignalAfterMs": 500
  }
}
```

The workload must stay open long enough for readiness polling, the new request,
and the repeated signal, while still finishing before the deadline.

## Common validation errors

| Message                                                                                                              | Fix                                              |
| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `command must be a non-empty array of strings, for example ["node", "server.js"]`                                    | Split the executable and arguments into an array |
| `baseUrl must use http://localhost, http://127.0.0.1 or http://[::1]`                                                | Use a local plain-HTTP origin                    |
| `readiness.path must be a local path beginning with one /`                                                           | Use `/health`, not a URL or `health`             |
| `workload.started.type must be response-headers or probe`                                                            | Choose one supported barrier                     |
| `workload.concurrent above 1 requires the response-headers start barrier, which verifies every request individually` | Use that barrier or one request                  |
| `shutdown.newRequests requires shutdown.readinessWithdrawal: true so rejection is tested after drain begins`         | Enable readiness withdrawal                      |
| `shutdown.repeatSignalAfterMs must be shorter than shutdown.deadlineMs`                                              | Lower the repeat delay or raise the deadline     |
| `v0.1 supports only SIGTERM`                                                                                         | Remove `signal` or set it to `SIGTERM`           |

The `v0.1` text is the literal message in package version 1.0.1. See
[troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting) for errors grouped by symptom.

## Can I use TypeScript instead of JSON?

The CLI reads JSON only. In a test or script, create a typed object with
`defineConfig()` and pass it to `checkShutdown()`:

```ts title="shutdown.test.ts"
import { checkShutdown, defineConfig } from "shutdown-check";

const config = defineConfig({
  command: ["node", "dist/server.js"],
  baseUrl: "http://127.0.0.1:3510",
  readiness: { path: "/health" },
  workload: {
    path: "/slow",
    started: { type: "response-headers" },
  },
  shutdown: { readinessWithdrawal: true },
});

const result = await checkShutdown(config);
```

The object follows the same rules and defaults. See the
[Node API](https://shutdown.jscrate.dev/docs/node-api) for validation and result handling.

## Related

- [Quick start](https://shutdown.jscrate.dev/docs/quick-start)
- [CLI reference](https://shutdown.jscrate.dev/docs/cli)
- [Node API](https://shutdown.jscrate.dev/docs/node-api)
- [In-flight work and start barriers](https://shutdown.jscrate.dev/docs/in-flight-work)
- [Diagnostic codes](https://shutdown.jscrate.dev/docs/codes)
