# How it works

> How shutdown-check tests graceful shutdown: it launches your real server, holds a request open, sends SIGTERM and checks the response, exit and port.

Source: https://shutdown.jscrate.dev/docs/how-it-works
Last updated: 2026-09-23

shutdown-check treats your service like a deployment platform does. It starts
the real command, sends HTTP traffic, delivers `SIGTERM`, and observes the
result from outside the process. It does not import your application or call
the shutdown handler directly.

## What is a black-box shutdown test?

A black-box shutdown test uses only the service's public behavior: its command,
port, HTTP routes, signal handling, and exit status. This covers problems that
unit tests often miss, including shell wrappers, framework behavior, real
sockets, process exit, and child processes.

The tradeoff is visibility. shutdown-check can see HTTP responses and process
events, but it cannot know whether an internal database write or queue message
finished unless the response proves it. See
[compatibility and limits](https://shutdown.jscrate.dev/docs/compatibility) for the exact boundary.

## What happens during a run?

The stages always run in this order:

| Stage | What shutdown-check does                              | Main failures |
| ----- | ----------------------------------------------------- | ------------- |
| 1     | Confirms the port is free                             | `SC001`       |
| 2     | Starts the configured command                         | `SC002`       |
| 3     | Waits for readiness                                   | `SC100–101`   |
| 4     | Opens work and proves it is active                    | `SC110–111`   |
| 5     | Confirms the process and work are still alive         | `SC112`       |
| 6     | Sends `SIGTERM`                                       | `SC113`       |
| 7     | Optionally repeats the signal                         | `SC114`       |
| 8     | Optionally checks readiness and new-request rejection | `SC310–312`   |
| 9     | Waits for active responses                            | `SC200–203`   |
| 10    | Checks the process exit                               | `SC300–301`   |
| 11    | Confirms the port is closed                           | `SC302`       |

The first reported failure identifies the earliest useful problem after all
required waits have settled. A pass is [SC000](https://shutdown.jscrate.dev/docs/codes/sc000).

## 1. Check that the port is free

Before launch, shutdown-check sends a short request to the readiness URL. The
connection must be refused. Any response, timeout, or unclear network error
means the port is not confirmed free and produces
[SC001](https://shutdown.jscrate.dev/docs/codes/sc001).

This prevents a false pass against a server that was already running. It also
makes parallel tests predictable: each test needs its own port.

## 2. Launch the real command

The command array is spawned directly without a shell. It runs in its own
process group, with `cwd` and `env` from the config.

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

Use the same entry point you use in production when possible. Starting the
server directly also avoids wrappers that swallow `SIGTERM` or exit before a
child server.

## 3. Wait for readiness

shutdown-check polls `baseUrl + readiness.path` until it receives the expected
status. Refused connections and other statuses are retried until
`readiness.timeoutMs`.

- The process stays alive but never becomes ready: [SC100](https://shutdown.jscrate.dev/docs/codes/sc100).
- The process exits or cannot start: [SC101](https://shutdown.jscrate.dev/docs/codes/sc101).

No workload is sent until readiness passes. This separates startup failures
from shutdown failures.

## 4. Prove that work is active

The test sends one or more workload requests, then waits for the configured
start barrier.

### Response-headers barrier

Every request must receive response headers while its body is still open. Use
this with streaming routes or controlled endpoints that flush headers before
finishing.

### Probe barrier

A separate route must change from `inactiveStatus` to `activeStatus` while the
workload request remains open. Use this when the handler sends its response
only after all work completes.

The check reports [SC110](https://shutdown.jscrate.dev/docs/codes/sc110) when the probe is already active,
or [SC111](https://shutdown.jscrate.dev/docs/codes/sc111) when work cannot be confirmed. The
[in-flight work guide](https://shutdown.jscrate.dev/docs/in-flight-work) includes complete examples.

## 5. Recheck the process and requests

There is a small gap between the barrier passing and the signal being sent.
shutdown-check verifies that the service and every workload request are still
active in that gap. If either ended, the result is
[SC112](https://shutdown.jscrate.dev/docs/codes/sc112).

This prevents a test from passing with a workload that was only barely slow
enough to reach the barrier.

## 6. Send SIGTERM

shutdown-check sends `SIGTERM` to the exact process it launched. It does not
signal the whole process group at this stage. That detail exposes wrappers:
the launcher must forward the signal to the real server.

If the operating system refuses the signal, the result is
[SC113](https://shutdown.jscrate.dev/docs/codes/sc113). On success, the shutdown deadline begins and the
timeline records `signal sent`.

## 7. Repeat the signal when requested

When `repeatSignalAfterMs` is set, shutdown-check sends another `SIGTERM` only
if the process and original requests are still active. This checks that a
second signal does not force an early exit.

If the repeated signal cannot be tested or delivered, the result is
[SC114](https://shutdown.jscrate.dev/docs/codes/sc114). Keep the repeat delay shorter than both the
workload duration and `shutdown.deadlineMs`.

## 8. Check traffic draining

These checks are optional.

With `readinessWithdrawal: true`, shutdown-check polls the readiness route
after the signal. A non-ready status or refused connection counts as
withdrawn. A timeout does not. If readiness never changes, the result is
[SC310](https://shutdown.jscrate.dev/docs/codes/sc310).

With `newRequests`, the test then sends one new `GET` while the original work
is still active. The request must return a configured rejection status or be
refused when that behavior is allowed.

- New work is accepted or times out: [SC311](https://shutdown.jscrate.dev/docs/codes/sc311).
- Original work ends before the rejection test: [SC312](https://shutdown.jscrate.dev/docs/codes/sc312).

See [readiness and draining](https://shutdown.jscrate.dev/docs/guides/readiness-and-draining) for a server
that returns `503` during this window.

## 9. Wait for each active response

Every workload request must finish before `shutdown.deadlineMs` with the
configured status and optional body text.

| Result                                          | Code                       |
| ----------------------------------------------- | -------------------------- |
| The request never ends                          | [SC200](https://shutdown.jscrate.dev/docs/codes/sc200) |
| The connection is reset or closed early         | [SC201](https://shutdown.jscrate.dev/docs/codes/sc201) |
| The final status differs from `workload.status` | [SC202](https://shutdown.jscrate.dev/docs/codes/sc202) |
| The body lacks `workload.bodyIncludes`          | [SC203](https://shutdown.jscrate.dev/docs/codes/sc203) |

Concurrent requests are checked individually. The first failing request in
request order supplies the code.

## 10. Check process exit

The service must exit before the same shutdown deadline.

- It is still running: [SC300](https://shutdown.jscrate.dev/docs/codes/sc300).
- Its code differs from `shutdown.exitCode`, or a signal ended it:
  [SC301](https://shutdown.jscrate.dev/docs/codes/sc301).

The default expected code is `0`. A planned shutdown should normally look like
a clean exit to process managers and monitoring.

## 11. Confirm that the port closed

After the launched process exits, shutdown-check tries the readiness URL once
more. The connection must be refused. If something still answers, a child
server is probably still running and the result is
[SC302](https://shutdown.jscrate.dev/docs/codes/sc302).

This is how the test catches `npm start`, shell scripts, or other launchers
that exit without stopping the actual server.

## How are failures cleaned up?

After any result, shutdown-check closes its open requests and clears timers.
If the service or a child process remains, the tool sends `SIGKILL` to the
process group it created. Cleanup prevents a failed test from leaving the port
occupied for the next run.

Cleanup is not a passing shutdown. A service passes only when it finishes its
own work, exits with the expected code, and closes the port before cleanup is
needed.

## How should I use the timeline?

Read it from top to bottom and find the last successful event before
`check failed`. For example:

- no `service ready` means startup or readiness failed;
- no `work confirmed active` means the start barrier failed;
- `signal sent` followed by no request completion means the drain hung;
- `process exited` while the port stays open means a child server survived.

The [output reference](https://shutdown.jscrate.dev/docs/output) lists every event and the fields available
in JSON.

## Related

- [Quick start](https://shutdown.jscrate.dev/docs/quick-start)
- [Configuration reference](https://shutdown.jscrate.dev/docs/configuration)
- [Diagnostic codes](https://shutdown.jscrate.dev/docs/codes)
- [Troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting)
- [Graceful shutdown in Node.js](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs)
