Shutdown Check

Search documentation

Find a page or section

Describe how to start the service, create active work, and judge the shutdown.

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:

npx shutdown-check init

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

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 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

OptionTypeDefaultDescription
command*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.
cwdstringthe config file's folderWorking directory for command, resolved relative to the config file (or to baseDirectory in the API).
envRecord<string, string>{}Environment variables added to, or overriding, the check's own environment for the launched process.
baseUrl*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*object—The route that says the service is ready to take traffic. Polled until it returns status.
readiness.path*string—Local path of the readiness route, such as /health.
readiness.statusnumber200The status that means ready. 100–599.
readiness.timeoutMsnumber10000How long to wait for the service to become ready. 100–300000 ms.
readiness.intervalMsnumber100Time between readiness polls. 10–10000 ms.
workload*object—The slow request that is in flight when SIGTERM arrives, and what its response must look like.
workload.path*string—Local path of an endpoint that takes long enough to still be running when the signal is sent.
workload.methodstring"GET"HTTP method, upper-cased.
workload.headersRecord<string, string>{}Request headers, as strings.
workload.bodystring—Request body, as a string.
workload.statusnumber200The status every in-flight request must finish with. Anything else is SC202.
workload.bodyIncludesstring—Text every in-flight response body must contain, to prove the work really completed. Missing text is SC203.
workload.concurrentnumber1How many workload requests to have in flight at once. 1–20; above 1 needs the response-headers barrier.
workload.started*object—How the check knows the work has really started before it sends SIGTERM: a start barrier. See start barriers.
workload.started.type*"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.timeoutMsnumber5000How long to wait for the work to start. 100–300000 ms. Not starting in time is SC111.
workload.started.pathstring—With type: "probe" (required there): the route that reports whether work is running.
workload.started.inactiveStatusnumber204With type: "probe": the status the probe returns while no work is running.
workload.started.activeStatusnumber200With type: "probe": the status it returns while work is running. Must differ from inactiveStatus.
workload.started.intervalMsnumber50With type: "probe": time between probe polls. 10–10000 ms.
shutdownobject—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.deadlineMsnumber10000How long, from the signal, the work has to finish and the process has to exit. 100–300000 ms.
shutdown.exitCodenumber0The exit code the process must exit with. 0–255.
shutdown.readinessWithdrawalbooleanfalseRequire the readiness route to stop returning its ready status (or refuse connections) after SIGTERM, before the deadline.
shutdown.newRequestsobject—Send one new GET during the drain and require it to be rejected. Needs readinessWithdrawal: true.
shutdown.newRequests.path*string—A safe, test-only route to send the new request to while old work drains.
shutdown.newRequests.rejectStatusesnumber[][503]Statuses that count as rejecting the new request.
shutdown.newRequests.allowConnectionRefusedbooleantrueWhether a refused connection also counts as a rejection. A timeout never does.
shutdown.repeatSignalAfterMsnumber—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

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:

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

Avoid a shell string or wrapper when possible:

{ "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.

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:

{ "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.

{
  "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.

AcceptedRejected
http://127.0.0.1:3510https://127.0.0.1:3510
http://localhost:3510http://example.com:3510
http://[::1]:3510http://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.

{
  "readiness": {
    "path": "/health",
    "status": 200,
    "timeoutMs": 30000,
    "intervalMs": 100
  }
}
FieldDefaultAllowed value
pathrequiredLocal path beginning with /
status200HTTP status from 100 to 599
timeoutMs10000100 to 300000
intervalMs10010 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. If it exits first, the result is 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.

{
  "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
    }
  }
}
FieldDefaultPurpose
pathrequiredLocal route used for the work
methodGETHTTP method
headers{}Request headers with string values
bodynoneRaw request body
status200Expected final response status
bodyIncludesnoneCase-sensitive text required in the response
concurrent1Number of requests, from 1 to 20
startedrequiredRule 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

{
  "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

{
  "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 for complete server examples and failure output.

Shutdown

The shutdown object defines the contract after the first SIGTERM.

{
  "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 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:

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

MessageFix
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 probeChoose one supported barrier
workload.concurrent above 1 requires the response-headers start barrier, which verifies every request individuallyUse that barrier or one request
shutdown.newRequests requires shutdown.readinessWithdrawal: true so rejection is tested after drain beginsEnable readiness withdrawal
shutdown.repeatSignalAfterMs must be shorter than shutdown.deadlineMsLower the repeat delay or raise the deadline
v0.1 supports only SIGTERMRemove signal or set it to SIGTERM

The v0.1 text is the literal message in package version 1.0.1. See 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():

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 for validation and result handling.