# Shutdown Check documentation > Test Node.js graceful shutdown in CI: shutdown-check sends SIGTERM to your real service mid-request and verifies in-flight work finishes before it exits. One package: `shutdown-check` (CLI and Node API; install with `npm install --save-dev shutdown-check`). Docs: https://shutdown.jscrate.dev. Source: https://github.com/re-sohail/shutdown-check. This file contains every documentation page in order. The index with one line per page is https://shutdown.jscrate.dev/llms.txt. --- # Quick start > Install shutdown-check, create a config, and test that your Node.js server finishes in-flight HTTP requests and exits cleanly when it receives SIGTERM. Source: https://shutdown.jscrate.dev/docs/quick-start Last updated: 2026-09-23 shutdown-check starts your Node.js service, opens a request, sends `SIGTERM` while that request is active, and watches what happens. A passing result means the request finished, the process exited on time, and the port closed. ## Before you begin You need: - Node.js 22 or later; - macOS or Linux; - a local HTTP/1 service; - a readiness route, such as `/health`; - a route that stays active long enough to receive `SIGTERM`. The package works with node:http, Express, Fastify, NestJS, and other Node.js frameworks. It does not add shutdown behavior to your app; it tests the behavior you already wrote. See [compatibility and limits](https://shutdown.jscrate.dev/docs/compatibility) before using HTTPS, HTTP/2, WebSockets, Windows, or remote services. ## 1. Install the package Add shutdown-check as a development dependency: ```bash npm install --save-dev shutdown-check ``` The package has no runtime dependencies and does not need to ship with your production application. ## 2. Create the config Run the initializer from the project root: ```bash npx shutdown-check init ``` The command creates `shutdown-check.json`. It will not overwrite an existing file. The generated file is: ```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 } } ``` Update these values: | Field | What to enter | | ---------------- | ------------------------------------------------------------------------- | | `command` | The direct command that starts the built service | | `baseUrl` | A local port reserved for this test | | `readiness.path` | A route that returns the expected status when the service is ready | | `workload.path` | A route that remains active long enough for the shutdown signal to arrive | Prefer a direct command such as `["node", "dist/server.js"]`. A shell or package-manager wrapper may receive the signal instead of the server. ## 3. Make the workload testable The starter config uses the `response-headers` start barrier. The workload route must send headers and keep the response body open. This lets shutdown-check prove that the request is active before it sends `SIGTERM`. Here is a complete server that matches the starter config: ```js title="server.js" const http = require("node:http"); const port = Number(process.env.PORT ?? 3000); const server = http.createServer((request, response) => { if (request.url === "/health") { response.writeHead(200, { "content-type": "text/plain" }); response.end("ok"); return; } if (request.url === "/slow") { response.writeHead(200, { "content-type": "text/plain" }); response.flushHeaders(); setTimeout(() => response.end("work complete\n"), 2000); return; } response.writeHead(404).end(); }); server.listen(port, "127.0.0.1", () => { console.log(`listening on http://127.0.0.1:${port}`); }); process.on("SIGTERM", () => { console.log("SIGTERM received; closing the server"); server.close((error) => { if (error) { console.error(error); process.exitCode = 1; } }); }); ``` `response.flushHeaders()` sends the `200` headers immediately. The body ends two seconds later. When `SIGTERM` arrives, `server.close()` stops accepting new connections and waits for the open response to finish. If your handler sends nothing until its work is complete, use a [probe start barrier](https://shutdown.jscrate.dev/docs/in-flight-work#use-the-probe-barrier) instead. A normal fast route is not useful: it may finish before the signal, which produces [SC111](https://shutdown.jscrate.dev/docs/codes/sc111). ## 4. Run the test ```bash npx shutdown-check test ``` The test now: 1. checks that the configured port is free; 2. starts `command` in its own process group; 3. waits for the readiness route; 4. opens the workload request and confirms that it is active; 5. sends `SIGTERM`; 6. waits for the response and process exit; 7. checks that the port is closed. The command exits with `0` after a pass, `1` after a shutdown failure, and `2` when invalid setup or configuration prevents the test from running. ## 5. Read a passing result The server above prints a result like this. Timings and process IDs vary. ```text PASS SC000: Graceful shutdown verified Timeline: + 5 ms process launched — pid=73661 + 112 ms service ready — HTTP 200 + 112 ms work request sent — #1 GET /slow + 113 ms work confirmed active — 1 response(s) sent headers; bodies still in progress + 113 ms signal sent — SIGTERM + 2118 ms work request finished — #1 HTTP 200 + 2125 ms process exited — code=0, signal=none + 2126 ms shutdown verified — work completed and service exited before deadline ``` The important sequence is: - `work confirmed active` appears before `signal sent`; - the request finishes after the signal; - the process exits with code `0`; - `shutdown verified` appears last. [SC000](https://shutdown.jscrate.dev/docs/codes/sc000) is the pass code. The [output reference](https://shutdown.jscrate.dev/docs/output) explains every timeline event. ## 6. See a real failure Remove the `SIGTERM` handler and run the command again. Node.js uses its default signal behavior and exits immediately, cutting off the open request: ```text FAIL SC201: In-flight request was interrupted: aborted Timeline: + 6 ms process launched — pid=73692 + 112 ms service ready — HTTP 200 + 113 ms work request sent — #1 GET /slow + 113 ms work confirmed active — 1 response(s) sent headers; bodies still in progress + 113 ms signal sent — SIGTERM + 116 ms process exited — code=null, signal=SIGTERM + 116 ms work request finished — #1 aborted + 116 ms check failed — SC201: In-flight request was interrupted: aborted Service stdout (last 8 KiB): listening on http://127.0.0.1:3000 ``` The first line gives the stable diagnostic code. Open [SC201](https://shutdown.jscrate.dev/docs/codes/sc201) for the causes and fix. On failures, the CLI also includes the last 8 KiB of the service's stdout and stderr. An invalid config looks different. It starts with `shutdown-check:`, has no SC code or timeline, and exits with `2`: ```text shutdown-check: Cannot read JSON config /path/shutdown-check.json: Error: ENOENT: no such file or directory, open '/path/shutdown-check.json' ``` Use [troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting) when the command does not reach a normal test result. ## 7. Strengthen the check The starter config covers one request, process exit, and port closure. You can also verify multiple requests, response content, readiness withdrawal, new request rejection, and repeated signals: ```json title="shutdown-check.json" { "command": ["node", "server.js"], "baseUrl": "http://127.0.0.1:3000", "readiness": { "path": "/health" }, "workload": { "path": "/slow", "bodyIncludes": "work complete", "concurrent": 2, "started": { "type": "response-headers" } }, "shutdown": { "deadlineMs": 10000, "readinessWithdrawal": true, "newRequests": { "path": "/health" }, "repeatSignalAfterMs": 500 } } ``` The example server also passes the readiness and new-request checks because `server.close()` refuses new connections. Applications that remain open while draining can return `503` instead; see [readiness and draining](https://shutdown.jscrate.dev/docs/guides/readiness-and-draining). ## 8. Add the test to CI Build the service first, then run: ```bash npx shutdown-check test --json --junit shutdown-result.xml ``` The nonzero exit code fails the job. The JUnit file can be published as a test report, while JSON keeps the complete result in the job log. The [CI guide](https://shutdown.jscrate.dev/docs/ci) includes GitHub Actions and GitLab examples. ## Related - [Configuration reference](https://shutdown.jscrate.dev/docs/configuration) - [How the check works](https://shutdown.jscrate.dev/docs/how-it-works) - [Graceful shutdown in Node.js](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) - [In-flight requests and start barriers](https://shutdown.jscrate.dev/docs/in-flight-work) - [All diagnostic codes](https://shutdown.jscrate.dev/docs/codes) --- # 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) --- # Comparison > Graceful shutdown testing tools compared: shutdown-check next to terminus, http-terminator, unit tests, kill-and-curl scripts and staging-only checks. Source: https://shutdown.jscrate.dev/docs/comparison Last updated: 2026-09-23 Shutdown libraries and shutdown tests solve different problems. A library adds signal handling and cleanup to your service. shutdown-check starts the real service and tests whether that behavior works from the outside. ## What is shutdown-check for? Use shutdown-check when you already have a signal handler—written by you, your framework, or a library—and want to verify the complete process lifecycle. It checks: - the production-like start command; - readiness; - a real active HTTP request; - `SIGTERM` delivery; - response completion; - optional traffic draining; - process exit and exit code; - final port closure. It does not install a signal handler or close application resources for you. ## Shutdown libraries Libraries such as `@godaddy/terminus`, `http-terminator`, `lightship`, and `close-with-grace` run inside the application. They may register signal listeners, close HTTP servers, manage timeouts, and provide cleanup hooks. Use a library when you need an implementation. Then test the configured result with shutdown-check. A correct library can still be used incorrectly: the wrong server object may be closed, cleanup may run in the wrong order, or the production command may prevent signals from reaching Node.js. ## Framework shutdown hooks Fastify, NestJS, and other frameworks provide shutdown APIs. These are also implementations, not end-to-end tests. Framework tests often call a close method directly. A deployment sends a signal to a process. shutdown-check covers the signal listener, command, framework, open socket, and final exit together. ## Unit tests A unit test can call the shutdown function and assert that mocked dependencies were closed. Strengths: - fast; - precise failure location; - easy to cover error branches; - suitable for database and queue cleanup logic. Limits: - usually no real child process; - no operating-system signal; - mocked HTTP connections; - no proof that the production start command exits; - no check for orphaned child processes. Keep unit tests. Add a black-box test for the process behavior they cannot cover. ## Shell scripts with kill and curl A custom script can start the app, use `curl`, send `kill -TERM`, and inspect the exit. This approach can work, but reliable synchronization is difficult. A fixed sleep does not prove that a request was active when the signal arrived. The script must also handle concurrent requests, response bodies, timeouts, process groups, cleanup, exit codes, and CI reports. shutdown-check packages those details into a repeatable config and stable diagnostic codes. ## Staging and Kubernetes tests A staging deployment is the only place to test cluster-level behavior such as endpoint propagation, ingress timing, sidecars, and `preStop` hooks. It is also slower and harder to reproduce. Requests may not be active during termination, and a successful rollout does not prove that no request was dropped. Use shutdown-check before deployment for deterministic server behavior, then use staging tests for infrastructure behavior. ## Comparison table | Capability | shutdown-check | Shutdown library | Unit test | Shell script | Staging test | | --------------------------------------- | -------------- | ---------------- | ---------- | ------------ | ------------ | | Adds shutdown behavior | No | Yes | No | No | No | | Runs the real start command | Yes | N/A | Usually no | Yes | Yes | | Sends a real `SIGTERM` | Yes | Handles it | Usually no | Yes | Yes | | Proves work is active before the signal | Yes | No | With mocks | Hard | Hard | | Verifies response status and body | Yes | No | Yes | Possible | Possible | | Checks readiness withdrawal | Optional | May implement | With mocks | Possible | Yes | | Checks process exit and final port | Yes | No | No | Possible | Indirect | | Checks databases and queues directly | No | May close them | Yes | Custom | Custom | | Covers cluster routing and sidecars | No | No | No | No | Yes | | Produces stable failure codes | Yes | No | Test names | Custom | Custom | ## Which approach should I use? For most services: 1. Use framework features, a library, or a small handler to implement shutdown. 2. Unit-test application-specific cleanup and error paths. 3. Run shutdown-check locally and in CI for the process and HTTP lifecycle. 4. Test Kubernetes, proxy, and load-balancer behavior in staging. These layers complement each other. Replacing all of them with one test leaves important behavior unverified. ## When shutdown-check is not enough Add another test when correctness depends on: - a database transaction after the HTTP response; - queue acknowledgement; - background jobs; - WebSockets or HTTP/2; - remote service coordination; - container or cluster termination order. The [compatibility page](https://shutdown.jscrate.dev/docs/compatibility) describes the boundary in more detail. ## Related - [How shutdown-check works](https://shutdown.jscrate.dev/docs/how-it-works) - [Graceful shutdown in Node.js](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) - [Kubernetes and containers](https://shutdown.jscrate.dev/docs/guides/kubernetes) - [Compatibility and limits](https://shutdown.jscrate.dev/docs/compatibility) - [Run in CI](https://shutdown.jscrate.dev/docs/ci) --- # FAQ > Node.js graceful shutdown FAQ: SIGTERM, server.close(), in-flight requests, frameworks, Docker and Kubernetes, and what shutdown-check does and does not test. Source: https://shutdown.jscrate.dev/docs/faq Last updated: 2026-09-23 These answers cover the most common Node.js graceful shutdown questions. Each answer links to a guide or reference page when you need working code or deeper troubleshooting. ## What is graceful shutdown in Node.js? Graceful shutdown is the process of stopping a service without cutting off work that already started. After `SIGTERM`, the service stops taking new work, finishes active requests, closes shared resources, and exits before the platform deadline. Node.js does not build this application-specific sequence for you. Add a signal handler or use a shutdown library, then verify the behavior. The [Node.js guide](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) includes complete node:http, Express, and Fastify examples. ## Does shutdown-check make my app shut down gracefully? No. shutdown-check is a test tool, not a shutdown-handler library. It does not change your service or run inside it. The tool starts the configured command, opens a real request, sends `SIGTERM`, and checks the response and process. You still need to implement the handler, close resources, and choose a drain strategy. ## Why does Node.js exit immediately on SIGTERM? Without an effective listener, the signal uses its default termination behavior. Any open HTTP connection can be cut off. Install a listener on the process that actually runs the server: ```js process.on("SIGTERM", () => { server.close((error) => { if (error) process.exitCode = 1; }); }); ``` If a shell or package-manager wrapper starts Node.js, the signal may stop at the wrapper. Prefer a direct command such as `node dist/server.js`. ## Why does server.close() not finish? `server.close()` waits for active connections. It can appear stuck when: - an HTTP response never ends; - an upstream or database operation hangs; - a client keeps a connection active; - the application closes a dependency that the request still needs; - another resource keeps Node.js alive after the server closes. Log the callback, active handlers, and every cleanup step. Add timeouts to external operations. [SC200](https://shutdown.jscrate.dev/docs/codes/sc200) covers a request that stays open; [SC300](https://shutdown.jscrate.dev/docs/codes/sc300) covers a process that stays alive after requests finish. ## Why does my test say the work was never in flight? The workload ended before the signal, or its start barrier never passed. A shutdown test cannot prove draining when no work is active. With `response-headers`, flush headers and keep the body open. With `probe`, make a separate route change from inactive to active while the operation runs. See [in-flight work and start barriers](https://shutdown.jscrate.dev/docs/in-flight-work). ## Does it work with Express, Fastify or NestJS? Yes. shutdown-check does not depend on a framework. It starts a command and uses local HTTP. - Express uses the Node.js server returned by `app.listen()`. - Fastify can close through `fastify.close()`. - NestJS needs its shutdown hooks configured and awaited. The service must expose readiness and workload routes and respond to `SIGTERM`. Framework-specific implementation is covered in the [graceful shutdown guide](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs). ## Does it test databases, queues or WebSockets? Not directly. shutdown-check observes the service process and HTTP/1 responses. It cannot see internal database or queue state, and it does not open WebSocket or HTTP/2 sessions. Use `bodyIncludes` when the response can prove that important work completed. Add application-specific tests for database transactions, queue acknowledgement, background tasks, WebSockets, and work that continues after the response. ## Does it work on Windows? Not natively. The tool requires POSIX signals and process groups, so it supports macOS and Linux. On a Windows workstation, run it in WSL, a Linux container, or Linux CI. The service and test must run in the same supported environment with access to the local port. ## Can I run it in Docker or Kubernetes? Yes, with limits. It can run inside a Linux container that has Node.js 22 or later. It tests the service process and local HTTP behavior inside that container. It does not simulate Kubernetes endpoint removal, `preStop`, sidecars, ingress, or the cluster grace period. Use the [Kubernetes guide](https://shutdown.jscrate.dev/docs/guides/kubernetes) to map those settings and test the remaining behavior in staging. ## How is it different from a shutdown library like terminus? A library such as terminus implements shutdown behavior inside the service. shutdown-check tests the behavior from outside the service. You can use both: a library to register handlers and close resources, then shutdown-check to prove the real start command, signal path, active request, exit, and port work together. See the [comparison page](https://shutdown.jscrate.dev/docs/comparison) for unit tests, scripts, and staging tests as well. ## Does shutdown-check send SIGINT? No. Version 1.0.1 supports `SIGTERM` only. A config with another signal is rejected. This matches the graceful termination path used by container runtimes and most process managers. ## Can it test several requests at once? Yes. Set `workload.concurrent` from 1 to 20 and use the `response-headers` start barrier. Every request must be active before the signal and must finish with the expected response. A `probe` barrier supports one request because a shared probe cannot identify which of several operations started. ## Why does the CLI exit with code 2 and no SC code? The test did not run. Exit code `2` is used for bad command-line arguments, missing or invalid JSON, and config validation errors. The stderr message starts with `shutdown-check:`. Fix that message first. SC codes are produced only after a valid test starts. See [troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting#the-cli-exits-with-code-2). ## How should I choose the shutdown deadline? The deadline must cover the slowest valid request and cleanup after `SIGTERM`, with a margin. It must also stay below the platform's forced-kill deadline. For Kubernetes, subtract `preStop` time from `terminationGracePeriodSeconds`. Do not use a longer deadline to hide work that can hang forever; add operation and cleanup timeouts. ## What should I run in CI? Build the application, then run: ```bash npx shutdown-check test --json --junit shutdown-result.xml ``` Exit code `1` fails the job for a shutdown problem. The JUnit report can be published with other test results. Use a dedicated port and isolated test data. ## 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) - [Compatibility and limits](https://shutdown.jscrate.dev/docs/compatibility) - [Troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting) --- # Releases > shutdown-check releases: the changelog for 1.0.1 and 1.0.0, the milestones built into 1.0, and how to upgrade and check the version you have installed. Source: https://shutdown.jscrate.dev/docs/releases Last updated: 2026-09-23 shutdown-check 1.0.1 is the current documented release. It adds CommonJS support and version flags to the first stable feature set released in 1.0.0. Release notes are on GitHub: https://github.com/re-sohail/shutdown-check/releases ## Version 1.0.1 This maintenance release adds: - a CommonJS entry point; - matching TypeScript declarations for CommonJS; - `shutdown-check --version`; - the `-v` short flag; - package metadata and documentation improvements. CommonJS projects can now use: ```js const { checkShutdown, defineConfig } = require("shutdown-check"); ``` ESM usage remains unchanged: ```ts import { checkShutdown, defineConfig } from "shutdown-check"; ``` ## Version 1.0.0 The first stable release includes: - real process launch and cleanup; - readiness polling; - response-headers and probe start barriers; - 1 to 20 active workload requests; - `SIGTERM` delivery and optional repetition; - readiness withdrawal and new-request rejection checks; - response status and body verification; - process deadline and exit-code verification; - final port-closure verification; - stable diagnostic codes; - text, JSON, and JUnit output; - CLI and typed Node.js API. ## Check the installed version ```bash npx shutdown-check --version ``` Or inspect the local dependency: ```sh npm list shutdown-check ``` Use the local project version in CI so the result matches development. ## Upgrade ```bash npm install --save-dev shutdown-check@latest ``` After upgrading: 1. Run `shutdown-check --version`. 2. Run the existing shutdown test locally. 3. Review the text timeline and exit code. 4. Regenerate or inspect JUnit output if CI consumes it. 5. Commit the lockfile change. Version 1.0.1 does not require a config migration from 1.0.0. ## Where does the changelog come from? Release information is derived from the package's `CHANGELOG.md` and published version metadata. The documentation keeps the supported CLI, API, and config aligned with the installed package through build-time checks. ## Related - [CLI reference](https://shutdown.jscrate.dev/docs/cli) - [Node API](https://shutdown.jscrate.dev/docs/node-api) - [Compatibility and limits](https://shutdown.jscrate.dev/docs/compatibility) - [Quick start](https://shutdown.jscrate.dev/docs/quick-start) --- # About > About shutdown-check: why it exists, what it is and is not, who maintains it, its MIT license, and how to report a problem with a useful, secret-free issue. Source: https://shutdown.jscrate.dev/docs/about Last updated: 2026-09-23 shutdown-check is a black-box test for one deployment question: will a Node.js HTTP service finish active requests and exit cleanly after `SIGTERM`? ## Why does the project exist? A shutdown handler can look correct in a code review and still fail in the real process. The start command may add a wrapper, the framework may handle sockets differently than expected, a request may still depend on a closing resource, or a timer may keep Node.js alive. Unit tests are valuable, but they often call the handler directly and mock the parts that fail during deployment. shutdown-check runs the actual command, opens a real HTTP connection, delivers the operating-system signal, and waits for the real exit. ## What shutdown-check is - A CLI and typed Node.js API. - A test for local HTTP/1 services. - A real process and signal test. - A check for active responses, optional traffic draining, process exit, and port closure. - A tool for local development and CI on macOS and Linux. ## What shutdown-check is not - It is not a shutdown-handler library. - It does not modify your application. - It is not tied to Express, Fastify, NestJS, or another framework. - It does not run a Kubernetes cluster or load balancer. - It does not inspect databases, queues, WebSockets, or HTTP/2 sessions. - It does not support native Windows signal behavior. The [comparison page](https://shutdown.jscrate.dev/docs/comparison) explains how it fits with shutdown libraries, unit tests, scripts, and staging tests. ## Design principles ### Test the real boundary The test observes the same inputs and outputs a deployment platform uses: command, port, HTTP, signal, and exit status. ### Do not create false passes Work must be proven active before `SIGTERM`. The port must be free before launch and closed after exit. Optional checks are performed only while the state they claim to test still exists. ### Make failures actionable Every planned failure has a stable SC code, a message, a timeline, and a page with causes and fixes. The CLI keeps the tail of service output for the same reason. ### Keep configuration explicit The config describes the command, routes, expected response, deadline, and drain behavior. It does not guess which endpoint or exit code is correct for your service. ## Project scope The current release focuses on: - `SIGTERM`; - local plain HTTP/1; - Node.js 22 or later; - macOS and Linux; - ESM and CommonJS; - text, JSON, and JUnit output. See [compatibility and limits](https://shutdown.jscrate.dev/docs/compatibility) before designing a test for protocols or environments outside that scope. ## Maintainer and license shutdown-check is maintained by Sohail Khan and released under the MIT license. The source, issue history, and releases are developed in public. The license allows use, modification, and distribution under its terms. Read the `LICENSE` file in the package or repository for the complete text. ## Report a problem Before opening an issue: 1. Run the latest version. 2. Read the page for the diagnostic code. 3. Check [troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting). 4. Reduce the problem to a small server and config when possible. Include: - shutdown-check version; - Node.js version; - operating system; - the command you ran; - config with secrets removed; - full result code, message, and timeline; - relevant captured stdout and stderr; - a reproduction repository or small server when available. Never include passwords, tokens, connection strings, private URLs, customer data, or other secrets. Replace them with clear placeholders. ## Related - [Quick start](https://shutdown.jscrate.dev/docs/quick-start) - [How shutdown-check works](https://shutdown.jscrate.dev/docs/how-it-works) - [Compatibility and limits](https://shutdown.jscrate.dev/docs/compatibility) - [Comparison](https://shutdown.jscrate.dev/docs/comparison) - [Releases](https://shutdown.jscrate.dev/docs/releases) --- # Graceful shutdown in Node.js > Node.js graceful shutdown: handle SIGTERM with server.close() in node:http, Express and Fastify, avoid the common mistakes, and verify it with shutdown-check. Source: https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs Last updated: 2026-09-23 A Node.js graceful shutdown begins when the process receives `SIGTERM`. The service stops taking new work, lets active requests finish, closes shared resources, and exits with code `0` before the platform deadline. ## What happens without a SIGTERM handler? On macOS and Linux, Node.js uses the default signal behavior and terminates. Open requests can lose their connections, and a proxy may turn that into a `502` or another upstream error. shutdown-check sees the interrupted request as [SC201](https://shutdown.jscrate.dev/docs/codes/sc201): ```text FAIL SC201: In-flight request was interrupted: aborted ``` Adding a signal listener removes the default immediate exit. From that point, your code is responsible for completing shutdown and allowing the process to exit. ## How do I handle SIGTERM in Node.js? A reliable handler follows this order: 1. Mark the instance as not ready. 2. Reject new work that still arrives. 3. Stop accepting new connections. 4. Wait for active HTTP requests. 5. Close databases, queues, workers, and timers. 6. Set a failure exit code only if cleanup fails. 7. Let Node.js exit when no handles remain. The exact order of steps 3–5 depends on the application. Do not close a database pool while active handlers still need it. ## node:http example ```js title="server.js" const http = require("node:http"); const port = Number(process.env.PORT ?? 3000); let draining = false; const server = http.createServer((request, response) => { if (request.url === "/health") { response.writeHead(draining ? 503 : 200, { "content-type": "text/plain", }); response.end(draining ? "draining" : "ok"); return; } if (draining) { response.writeHead(503, { connection: "close", "retry-after": "1", }); response.end("shutting down"); return; } if (request.url === "/slow") { response.writeHead(200, { "content-type": "text/plain" }); response.flushHeaders(); setTimeout(() => response.end("work complete\n"), 2000); return; } response.writeHead(404).end(); }); server.listen(port, "127.0.0.1"); process.on("SIGTERM", () => { if (draining) return; draining = true; server.close((error) => { if (error) { console.error("HTTP shutdown failed", error); process.exitCode = 1; } }); }); ``` The `draining` guard makes repeated `SIGTERM` events harmless. Requests that entered before the flag changed continue in their existing handlers. New requests receive `503` until the listener closes. `server.close()` stops new connections and waits for active requests. Its callback runs after the server closes, but other resources may still keep the event loop alive. ## Express graceful shutdown Express uses a Node.js HTTP server underneath. Keep the object returned by `app.listen()` and close it from the signal handler. ```js title="server.js" const express = require("express"); const app = express(); const port = Number(process.env.PORT ?? 3000); let draining = false; app.get("/health", (_request, response) => { response.status(draining ? 503 : 200).send(draining ? "draining" : "ok"); }); app.use((_request, response, next) => { if (!draining) return next(); response.set("connection", "close"); response.set("retry-after", "1"); response.status(503).send("shutting down"); }); app.get("/slow", async (_request, response) => { response.status(200); response.flushHeaders(); await new Promise((resolve) => setTimeout(resolve, 2000)); response.end("work complete\n"); }); const server = app.listen(port, "127.0.0.1"); process.on("SIGTERM", () => { if (draining) return; draining = true; server.close((error) => { if (error) { console.error(error); process.exitCode = 1; } }); }); ``` Place the draining middleware before application routes that should reject new work, but after the readiness route so it can return a clear readiness status. ## Fastify graceful shutdown Fastify provides `fastify.close()`, which stops accepting requests and runs registered `onClose` hooks. ```js title="server.js" const Fastify = require("fastify"); const fastify = Fastify(); const port = Number(process.env.PORT ?? 3000); let draining = false; fastify.get("/health", async (_request, reply) => { reply.code(draining ? 503 : 200); return draining ? "draining" : "ok"; }); fastify.get("/slow", async (_request, reply) => { reply.raw.writeHead(200, { "content-type": "text/plain" }); reply.raw.flushHeaders(); await new Promise((resolve) => setTimeout(resolve, 2000)); reply.raw.end("work complete\n"); }); await fastify.listen({ port, host: "127.0.0.1" }); process.on("SIGTERM", async () => { if (draining) return; draining = true; try { await fastify.close(); } catch (error) { fastify.log.error(error); process.exitCode = 1; } }); ``` Register database and queue cleanup with Fastify hooks or await it after HTTP work has drained. ## Close application resources An HTTP server can close while Node.js remains alive. After active requests no longer need them, close: - database and cache pools; - message consumers and producers; - workers and child processes; - scheduled intervals; - file watchers; - telemetry exporters; - long-lived clients. Use timeouts for cleanup that depends on another system. Without a timeout, the service may remain alive until the platform sends `SIGKILL`, reported by shutdown-check as [SC300](https://shutdown.jscrate.dev/docs/codes/sc300). ## Handle keep-alive connections `server.close()` stops new connections and waits for active requests. Modern Node.js versions also close idle keep-alive connections. If your stack tracks sockets itself, do not destroy every socket on `SIGTERM`; active sockets carry the requests you are trying to preserve. If you explicitly close idle connections, do it after the listener begins closing and keep active connections untouched. ## Add a hard safety timeout carefully Production services often need a final timeout so a broken cleanup step does not run forever. The timeout must be longer than the normal drain and shorter than the platform's forced-kill deadline. ```js const forceExit = setTimeout(() => { console.error("shutdown deadline exceeded"); process.exit(1); }, 25_000); forceExit.unref(); ``` An unref'ed timer does not keep an otherwise finished process alive. A forced exit can still cut off work, so treat it as a last resort and monitor when it runs. ## Common mistakes | Mistake | What users see | shutdown-check result | | ------------------------------------------- | ---------------------------------------- | --------------------- | | No signal handler | Requests reset during deployment | `SC201` or `SC301` | | `process.exit()` in the handler | Active work is cut off | `SC201` | | Database closes before requests finish | Handler error or wrong response | `SC202` or `SC203` | | Server closes but another handle stays open | Process reaches the platform deadline | `SC300` | | Wrapper does not forward signals | Parent exits while child server survives | `SC302` | | Readiness stays at `200` | New traffic continues to arrive | `SC310` | | New work is still accepted | Late requests may be cut off | `SC311` | | Second signal forces exit | Existing requests are interrupted | `SC114` or `SC201` | ## Verify the handler Use a config that checks the full behavior: ```json title="shutdown-check.json" { "command": ["node", "server.js"], "baseUrl": "http://127.0.0.1:3000", "readiness": { "path": "/health" }, "workload": { "path": "/slow", "bodyIncludes": "work complete", "started": { "type": "response-headers" } }, "shutdown": { "deadlineMs": 10000, "readinessWithdrawal": true, "newRequests": { "path": "/slow", "rejectStatuses": [503] }, "repeatSignalAfterMs": 500 } } ``` Run: ```bash npx shutdown-check test ``` A passing timeline should show active work before the signal, readiness and new-request rejection after it, request completion, and a clean process exit. ## Related - [Quick start](https://shutdown.jscrate.dev/docs/quick-start) - [Readiness and draining](https://shutdown.jscrate.dev/docs/guides/readiness-and-draining) - [In-flight work and barriers](https://shutdown.jscrate.dev/docs/in-flight-work) - [Kubernetes and containers](https://shutdown.jscrate.dev/docs/guides/kubernetes) - [Troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting) --- # In-flight work and start barriers > Test in-flight requests during shutdown: use a start barrier, response headers or a probe route, to prove work is running when SIGTERM arrives. Source: https://shutdown.jscrate.dev/docs/in-flight-work Last updated: 2026-09-23 To test in-flight requests during shutdown, shutdown-check must know that the request is still active. A start barrier supplies that proof. The signal is sent only after the barrier passes. ## Why is a start barrier required? Without a barrier, a fast request could finish before `SIGTERM`. The service would then pass even if it had no shutdown handler because there was nothing left to drain. shutdown-check prevents that false result. If it cannot prove active work, it returns [SC111](https://shutdown.jscrate.dev/docs/codes/sc111) and sends no signal. ## Choose a barrier | Barrier | It passes when | Use it when | Requests | | ------------------ | ------------------------------------------------------- | ------------------------------------------------------- | -------- | | `response-headers` | Every response sent headers while its body remains open | The route can stream or delay its body | 1–20 | | `probe` | A separate route changes from inactive to active | The handler sends nothing until its operation completes | 1 | `response-headers` proves that an HTTP response is open. A probe can prove that a specific internal operation has begun. ## Use the response-headers barrier Configure the workload: ```json title="shutdown-check.json" { "workload": { "path": "/slow", "concurrent": 2, "started": { "type": "response-headers", "timeoutMs": 5000 } } } ``` Each request must receive headers within `timeoutMs`, and its response body must still be open. With two requests, both must satisfy the barrier. ### Build a suitable route Node.js may hold headers until the first body write. Call `flushHeaders()` to send them immediately: ```js title="server.js" if (request.url === "/slow") { response.writeHead(200, { "content-type": "text/plain" }); response.flushHeaders(); setTimeout(() => { response.end("work complete\n"); }, 2000); return; } ``` The delay must be long enough for shutdown-check to send the signal, but shorter than `shutdown.deadlineMs`. The route should be safe to run repeatedly and should not modify production data. ### Understand failure behavior The barrier fails when: - a response ends before all headers are confirmed; - one request in a concurrent group ends early; - headers do not arrive before `timeoutMs`; - the route refuses or resets the connection. A successful timeline contains: ```text + 114 ms work confirmed active — 2 response(s) sent headers; bodies still in progress ``` ## Use the probe barrier Choose a probe when the handler computes its full result before sending any response. The probe reports whether that operation is currently running. ```json title="shutdown-check.json" { "command": ["node", "server.js"], "env": { "ENABLE_TEST_ROUTES": "1" }, "baseUrl": "http://127.0.0.1:3000", "readiness": { "path": "/health" }, "workload": { "path": "/reports", "method": "POST", "status": 200, "bodyIncludes": "report ready", "started": { "type": "probe", "path": "/test/work-active", "inactiveStatus": 204, "activeStatus": 200, "timeoutMs": 5000, "intervalMs": 50 } }, "shutdown": { "deadlineMs": 10000 } } ``` The probe runs in two phases: 1. Before the workload, it must return `inactiveStatus`. 2. After the workload starts, it must return `activeStatus` while the workload response is still open. If the first phase fails, the result is [SC110](https://shutdown.jscrate.dev/docs/codes/sc110). If the second phase times out or the workload ends first, the result is [SC111](https://shutdown.jscrate.dev/docs/codes/sc111). ### Build a probe route Track the real operation with a counter. Expose the route only in the test environment. ```js title="server.js" const http = require("node:http"); const { setTimeout: delay } = require("node:timers/promises"); let activeReports = 0; async function buildReport() { await delay(2000); return { rows: 42 }; } const server = http.createServer(async (request, response) => { if (request.url === "/health") { response.writeHead(200).end("ok"); return; } if ( request.url === "/test/work-active" && process.env.ENABLE_TEST_ROUTES === "1" ) { response.writeHead(activeReports > 0 ? 200 : 204).end(); return; } if (request.url === "/reports" && request.method === "POST") { activeReports++; try { const report = await buildReport(); response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ status: "report ready", ...report })); } finally { activeReports--; } return; } response.writeHead(404).end(); }); server.listen(3000, "127.0.0.1"); process.on("SIGTERM", () => server.close()); ``` The counter changes before the slow operation and resets in `finally`, so the probe reflects failures as well as success. ### Read a passing probe timeline ```text PASS SC000: Graceful shutdown verified Timeline: + 6 ms process launched — pid=73999 + 115 ms service ready — HTTP 200 + 116 ms work request sent — #1 POST /reports + 117 ms work confirmed active — probe /test/work-active returned HTTP 200 + 117 ms signal sent — SIGTERM + 2120 ms work request finished — #1 HTTP 200 + 2122 ms process exited — code=0, signal=none + 2123 ms shutdown verified — work completed and service exited before deadline ``` ## Prove that the operation completed A status `200` can still contain an error response. Set `bodyIncludes` to text that appears only after successful work: ```json { "bodyIncludes": "report ready" } ``` The comparison is case-sensitive. A missing marker returns [SC203](https://shutdown.jscrate.dev/docs/codes/sc203). Choose stable text, not a generated ID or timestamp. ## Design test-only routes safely - Enable them only through a test environment variable. - Do not expose internal state in production. - Avoid real billing, email, or destructive side effects. - Reset counters in `finally`. - Keep the workload deterministic on slow CI machines. - Give parallel tests isolated ports and state. ## Fix common barrier failures | Symptom | Likely cause | Fix | | ----------------------------------- | -------------------------------------- | --------------------------------------------------- | | Response finishes immediately | Route is too fast | Delay or stream the controlled test response | | Headers never arrive | Node.js is buffering them | Call `flushHeaders()` or write a body chunk | | Probe starts active | Old work or incorrect probe state | Reset state and verify `inactiveStatus` first | | Probe never becomes active | Counter changes too late or wrong path | Set state before the operation and verify the route | | CI fails but local passes | Timing window is too small | Make workload duration clearly longer | | Multiple requests reject the config | Probe supports one request | Use `response-headers` for concurrency | ## Related - [Workload and barrier configuration](https://shutdown.jscrate.dev/docs/configuration#start-barrier) - [SC110: Probe already active](https://shutdown.jscrate.dev/docs/codes/sc110) - [SC111: Work was never in flight](https://shutdown.jscrate.dev/docs/codes/sc111) - [SC203: Response body was wrong](https://shutdown.jscrate.dev/docs/codes/sc203) - [How the test runs](https://shutdown.jscrate.dev/docs/how-it-works) --- # Readiness and draining traffic > Make your readiness probe during shutdown return 503, reject new requests while old ones drain, and verify both with shutdown-check's drain checks. Source: https://shutdown.jscrate.dev/docs/guides/readiness-and-draining Last updated: 2026-09-23 When `SIGTERM` arrives, the service should stop reporting ready before it finishes active requests. Load balancers can then move traffic elsewhere while the instance drains. ## How do I drain connections before shutdown? Use this sequence: 1. Set an application-wide draining state. 2. Make the readiness route return a non-ready status. 3. Reject new application work. 4. Let requests that started earlier finish. 5. Close the listener and shared resources. 6. Exit before the platform deadline. Readiness changes are not immediate everywhere. A load balancer may poll only every few seconds, and endpoint updates take time to propagate. The service must handle requests that arrive during that delay. ## Choose a drain pattern | Pattern | Readiness after signal | New requests after signal | When to use it | | --------------------------------------- | ---------------------- | ------------------------- | ------------------------------------------------ | | Close the listener immediately | Connection refused | Connection refused | Clients retry and no readiness propagation delay | | Keep listening and return `503` briefly | `503` | `503` | Load balancers need time to observe readiness | Both can pass shutdown-check. Returning `503` provides a clearer response to clients and allows a short window for traffic systems to update. ## Build a server that drains ```js title="server.js" const http = require("node:http"); const port = Number(process.env.PORT ?? 3000); const drainDelayMs = Number(process.env.DRAIN_DELAY_MS ?? 1000); let draining = false; const server = http.createServer((request, response) => { if (request.url === "/health") { response.writeHead(draining ? 503 : 200, { "content-type": "text/plain", }); response.end(draining ? "draining" : "ok"); return; } if (draining) { response.writeHead(503, { connection: "close", "retry-after": "1", }); response.end("shutting down"); return; } if (request.url === "/slow") { response.writeHead(200, { "content-type": "text/plain" }); response.flushHeaders(); setTimeout(() => response.end("work complete\n"), 2000); return; } if (request.url === "/test/new-work") { response.writeHead(200).end("accepted"); return; } response.writeHead(404).end(); }); server.listen(port, "127.0.0.1"); process.on("SIGTERM", () => { if (draining) return; draining = true; setTimeout(() => { server.close((error) => { if (error) { console.error(error); process.exitCode = 1; } }); }, drainDelayMs); }); ``` Requests already inside a handler do not check `draining` again, so they finish normally. New requests pass through the draining guard and receive `503`. The delay keeps the listener available while traffic systems observe the readiness change. After that, `server.close()` stops connections and waits for the original work. ## Configure the drain checks ```json title="shutdown-check.json" { "command": ["node", "server.js"], "baseUrl": "http://127.0.0.1:3000", "readiness": { "path": "/health", "status": 200 }, "workload": { "path": "/slow", "bodyIncludes": "work complete", "concurrent": 2, "started": { "type": "response-headers" } }, "shutdown": { "deadlineMs": 10000, "readinessWithdrawal": true, "newRequests": { "path": "/test/new-work", "rejectStatuses": [503], "allowConnectionRefused": true }, "repeatSignalAfterMs": 500 } } ``` ## What readinessWithdrawal checks After `SIGTERM`, shutdown-check polls the normal readiness route. The check passes when it sees: - any status other than `readiness.status`; or - a refused or reset connection. A request timeout does not count. It does not clearly tell a load balancer that the service is unavailable. If readiness stays ready until the deadline, shutdown-check returns [SC310](https://shutdown.jscrate.dev/docs/codes/sc310). ## What newRequests checks After readiness is withdrawn, shutdown-check sends one `GET` to `newRequests.path` while the original workload remains active. The new request must: - return a status in `rejectStatuses`; or - be refused when `allowConnectionRefused` is `true`. A timeout, `200`, or another unlisted status returns [SC311](https://shutdown.jscrate.dev/docs/codes/sc311). If the original work ends before the new request can be tested, the result is [SC312](https://shutdown.jscrate.dev/docs/codes/sc312). Choose a safe `GET` route. shutdown-check really calls it, so it must not create orders, send email, or change production data. ## Read a passing timeline ```text PASS SC000: Graceful shutdown verified Timeline: + 5 ms process launched — pid=73882 + 112 ms service ready — HTTP 200 + 112 ms work request sent — #1 GET /slow + 112 ms work request sent — #2 GET /slow + 113 ms work confirmed active — 2 response(s) sent headers; bodies still in progress + 113 ms signal sent — SIGTERM + 114 ms readiness withdrawn — HTTP 503 + 115 ms new request rejected — HTTP 503 + 615 ms signal repeated — SIGTERM + 2115 ms work request finished — #1 HTTP 200 + 2115 ms work request finished — #2 HTTP 200 + 2118 ms process exited — code=0, signal=none + 2119 ms shutdown verified — work completed and service exited before deadline ``` The key ordering is readiness withdrawal and new-request rejection before the original work finishes. ## Choose the drain delay Start with the load balancer's readiness polling interval multiplied by its failure threshold. Add time for endpoint propagation where relevant. The total must still fit: ```text drain delay + slowest active request + cleanup < platform shutdown deadline ``` Set `shutdown.deadlineMs` to cover the server-side portion with a safety margin. On Kubernetes, remember that `preStop` time is included in `terminationGracePeriodSeconds`. ## Handle repeated SIGTERM safely Signal handlers should be idempotent. The first signal starts the drain; later signals should not restart timers, call cleanup twice, or force active work to exit. The `if (draining) return` guard in the example handles this. Enable `repeatSignalAfterMs` to test the behavior. ## Common failures | Failure | Result | Fix | | ---------------------------------------- | ------- | -------------------------------------------------------- | | Readiness remains `200` | `SC310` | Flip readiness as the first signal-handling step | | New request returns `200` | `SC311` | Add a draining guard before application routes | | New request hangs | `SC311` | Return a clear rejection status | | Old work ends before rejection is tested | `SC312` | Use longer controlled work or withdraw readiness sooner | | Old request receives `503` | `SC202` | Apply the guard only when requests enter after draining | | Process stays alive | `SC300` | Close timers and shared resources after HTTP work drains | ## Related - [Graceful shutdown in Node.js](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) - [Kubernetes and containers](https://shutdown.jscrate.dev/docs/guides/kubernetes) - [Shutdown configuration](https://shutdown.jscrate.dev/docs/configuration#shutdown) - [SC310: Readiness not withdrawn](https://shutdown.jscrate.dev/docs/codes/sc310) - [SC311: New request accepted](https://shutdown.jscrate.dev/docs/codes/sc311) --- # Kubernetes and containers > Kubernetes graceful shutdown for Node.js: how pods stop, preStop and terminationGracePeriodSeconds, PID 1 in Docker, and a matching shutdown-check config. Source: https://shutdown.jscrate.dev/docs/guides/kubernetes Last updated: 2026-09-23 Kubernetes graceful shutdown for Node.js requires the server to stop receiving traffic, finish active requests, and exit before the pod's grace period ends. shutdown-check tests the server-side part locally or in CI; it does not emulate the Kubernetes control plane. ## What happens when a pod stops? During deletion, a rolling update, scale-down, or node drain: 1. Kubernetes marks the pod as terminating. 2. Endpoint removal begins so Services and load balancers can stop routing to the pod. 3. The kubelet runs a configured `preStop` hook. 4. The container runtime sends `SIGTERM` to the container's main process. 5. Kubernetes waits for the container to exit. 6. At the end of `terminationGracePeriodSeconds`, remaining processes receive `SIGKILL`. Endpoint updates and container termination begin around the same time. Traffic can still arrive after the signal because kube-proxy, ingress controllers, and external load balancers need time to observe the change. ## What must the service do? The server should: - change readiness immediately; - reject new work that still arrives; - preserve requests that began before the drain; - close shared resources after active handlers finish; - exit with code `0` before the grace period ends. Closing the listener immediately may be enough when every client retries and traffic routing updates quickly. A safer zero downtime deploy often uses a short drain window that returns `503` before closing the listener. ## Configure readiness and preStop ```yaml title="deployment.yaml" spec: template: spec: terminationGracePeriodSeconds: 30 containers: - name: api image: example/api:1.4.2 command: ["node", "dist/server.js"] ports: - containerPort: 3000 readinessProbe: httpGet: path: /health port: 3000 periodSeconds: 2 failureThreshold: 1 lifecycle: preStop: exec: command: ["sleep", "5"] ``` The `preStop` delay gives endpoint updates time to propagate before `SIGTERM`. It uses part of the 30-second grace period; it does not add extra time. The exec form requires a `sleep` binary in the image. Some Kubernetes versions offer a built-in sleep action. Confirm support in the cluster before depending on it. An application-level drain can replace or supplement `preStop`. The readiness route returns `503` as soon as `SIGTERM` arrives, and the server keeps listening briefly before calling `server.close()`. ## Match Kubernetes to shutdown-check | Kubernetes setting or behavior | shutdown-check setting | | ------------------------------------ | ----------------------------------------------------- | | Container `command` | `command` | | `readinessProbe.httpGet.path` | `readiness.path` | | Pod grace period | Upper bound for `shutdown.deadlineMs` | | `preStop` duration | Time to subtract from the server's available deadline | | Requests arriving during termination | `shutdown.newRequests` | | Slowest valid application request | `workload` | For a 30-second grace period with a 5-second `preStop`, a 20-second test deadline leaves about 5 seconds of safety margin: ```json title="shutdown-check.json" { "command": ["node", "dist/server.js"], "env": { "NODE_ENV": "production", "PORT": "3000" }, "baseUrl": "http://127.0.0.1:3000", "readiness": { "path": "/health" }, "workload": { "path": "/test/slow", "bodyIncludes": "work complete", "concurrent": 3, "started": { "type": "response-headers" } }, "shutdown": { "deadlineMs": 20000, "readinessWithdrawal": true, "newRequests": { "path": "/test/new-work", "rejectStatuses": [503] }, "repeatSignalAfterMs": 1000 } } ``` shutdown-check begins at `SIGTERM`, so it does not run or time the `preStop` hook. The deadline should describe the time available to the application after the hook. ## Why does PID 1 matter? The container command becomes PID 1. Signal and child-process behavior around PID 1 is different from an ordinary process, so choose the command carefully. ### Use exec form Preferred Dockerfile command: ```dockerfile CMD ["node", "dist/server.js"] ``` Avoid shell form: ```dockerfile CMD node dist/server.js ``` Shell form starts `/bin/sh -c` as PID 1. The shell may not forward `SIGTERM` to Node.js. ### Avoid unnecessary package-manager wrappers This adds another process between Kubernetes and the server: ```dockerfile CMD ["npm", "start"] ``` Starting `node` directly makes signal ownership clear. If a wrapper is required, it must forward signals and wait for the child. ### Consider a minimal init process An init such as tini can forward signals and reap orphaned child processes. It is useful when the application spawns children. Configure it intentionally and test the exact container command used in production. ## Diagnose wrapper problems Use the production command in shutdown-check. The result often reveals the failure before you build an image: | Symptom | Code | | ------------------------------------------- | -------------------------- | | Wrapper exits while child server keeps port | [SC302](https://shutdown.jscrate.dev/docs/codes/sc302) | | Wrapper is killed by the signal | [SC301](https://shutdown.jscrate.dev/docs/codes/sc301) | | Process remains alive through the deadline | [SC300](https://shutdown.jscrate.dev/docs/codes/sc300) | | Active request loses its connection | [SC201](https://shutdown.jscrate.dev/docs/codes/sc201) | ## Run shutdown-check inside a container You can run the tool inside a Linux image when it contains Node.js 22 or later and the package. shutdown-check starts the configured service as a child and connects to it on loopback. This verifies: - the built image can start the service; - its runtime files and environment are present; - the Node.js process handles `SIGTERM`; - active local HTTP requests drain; - the process and port close. It does not verify: - Kubernetes endpoint removal; - an actual `preStop` hook; - ingress or external load-balancer timing; - sidecar termination order; - cluster-specific grace-period behavior. Use a staging cluster for those integration checks. ## Handle sidecars and service meshes A sidecar proxy can continue or stop forwarding traffic independently of the Node.js process. shutdown-check talks directly to the local service and does not model sidecar shutdown order. When using a mesh, confirm: - readiness reflects both proxy and application state; - the proxy keeps forwarding existing connections during the app drain; - sidecar termination does not shorten the application deadline; - retries do not hide repeated failures. ## Set a realistic grace period Budget the full termination window: ```text preStop + routing delay + slowest active request + resource cleanup + margin ``` If the total exceeds `terminationGracePeriodSeconds`, Kubernetes will send `SIGKILL` before the service finishes. Raising the grace period can be valid, but also fix handlers, queries, or cleanup operations that can hang forever. ## Related - [Readiness and draining](https://shutdown.jscrate.dev/docs/guides/readiness-and-draining) - [Graceful shutdown in Node.js](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) - [Run shutdown-check in CI](https://shutdown.jscrate.dev/docs/ci) - [Compatibility and limits](https://shutdown.jscrate.dev/docs/compatibility) - [SC302: Port still open](https://shutdown.jscrate.dev/docs/codes/sc302) --- # Tests with node:test or Vitest > Test graceful shutdown with node:test or Vitest: call checkShutdown from a test, set a long enough timeout, assert the result and print the timeline. Source: https://shutdown.jscrate.dev/docs/guides/test-runners Last updated: 2026-09-23 Use `checkShutdown()` when graceful shutdown should run with the rest of your tests. The function starts and cleans up the service, so the test needs no separate server setup or teardown. ## Test with node:test ```js title="test/shutdown.test.mjs" import assert from "node:assert/strict"; import { test } from "node:test"; import { checkShutdown, defineConfig } from "shutdown-check"; const config = defineConfig({ command: ["node", "server.js"], env: { PORT: "3510" }, baseUrl: "http://127.0.0.1:3510", readiness: { path: "/health", timeoutMs: 10_000 }, workload: { path: "/slow", bodyIncludes: "work complete", started: { type: "response-headers" }, }, shutdown: { deadlineMs: 10_000, readinessWithdrawal: true, }, }); test("drains active requests on SIGTERM", { timeout: 30_000 }, async (t) => { const result = await checkShutdown(config); if (!result.pass) { for (const item of result.timeline) { const detail = item.detail ? ` — ${item.detail}` : ""; t.diagnostic(`+${item.ms} ms ${item.event}${detail}`); } if (result.stderr) t.diagnostic(result.stderr); if (result.stdout) t.diagnostic(result.stdout); } assert.equal(result.pass, true, `${result.code}: ${result.message}`); }); ``` Run the file: ```sh node --test test/shutdown.test.mjs ``` The assertion message keeps the stable code and explanation in the test failure. Diagnostics add the timeline and service output only when needed. ## Test with Vitest ```ts title="test/shutdown.test.ts" import { checkShutdown, defineConfig } from "shutdown-check"; import { expect, test } from "vitest"; const config = defineConfig({ command: ["node", "server.js"], env: { PORT: "3511" }, baseUrl: "http://127.0.0.1:3511", readiness: { path: "/health", timeoutMs: 10_000 }, workload: { path: "/slow", bodyIncludes: "work complete", started: { type: "response-headers" }, }, shutdown: { deadlineMs: 10_000, readinessWithdrawal: true, }, }); test("drains active requests on SIGTERM", async () => { const result = await checkShutdown(config); if (!result.pass) { console.error(result.timeline); if (result.stderr) console.error(result.stderr); if (result.stdout) console.error(result.stdout); } expect(result.pass, `${result.code}: ${result.message}`).toBe(true); }, 30_000); ``` Run: ```sh npx vitest run test/shutdown.test.ts ``` Vitest's default timeout is normally too short for a real process lifecycle, so pass a timeout to the test or set one in the Vitest config. ## Choose the test timeout The outer test timeout must exceed: ```text readiness.timeoutMs + workload.started.timeoutMs + shutdown.deadlineMs + process overhead ``` For defaults, 30 seconds is a reasonable test-runner timeout. This does not mean every run takes 30 seconds; a normal run ends when the service exits. If the test runner times out first, it may hide the shutdown-check result and timeline. Keep the outer timeout generous while keeping the shutdown deadline close to the real production limit. ## Assert the useful value `checkShutdown()` resolves after an expected shutdown failure. Assert `result.pass`, not only whether the promise resolves. ```js assert.equal(result.pass, true, `${result.code}: ${result.message}`); ``` For a negative test that proves a broken server is detected, assert the code: ```js assert.equal(result.code, "SC311"); ``` Avoid matching the full message unless the exact text is part of your test. The diagnostic code is the stable failure identifier. ## Handle invalid config Invalid input causes `checkShutdown()` to reject with an `Error` before the test runs. ```ts await assert.rejects( () => checkShutdown(invalidConfig), /baseUrl must use http:\/\// ); ``` Use `defineConfig()` for editor feedback, but remember that it does not perform runtime validation. `checkShutdown()` validates when called. ## Use CommonJS ```js title="test/shutdown.test.cjs" const assert = require("node:assert/strict"); const { test } = require("node:test"); const { checkShutdown } = require("shutdown-check"); test("drains active requests", { timeout: 30_000 }, async () => { const result = await checkShutdown({ command: ["node", "server.js"], env: { PORT: "3512" }, baseUrl: "http://127.0.0.1:3512", readiness: { path: "/health" }, workload: { path: "/slow", started: { type: "response-headers" }, }, }); assert.equal(result.pass, true, `${result.code}: ${result.message}`); }); ``` CommonJS support is available in shutdown-check 1.0.1 and later. ## Give every test its own port Test runners may execute files in parallel. Two shutdown checks cannot share a port because each run requires the port to be free before launch. Use separate ports: ```ts const ordersConfig = defineConfig({ env: { PORT: "3510" }, baseUrl: "http://127.0.0.1:3510", // ... }); const billingConfig = defineConfig({ env: { PORT: "3511" }, baseUrl: "http://127.0.0.1:3511", // ... }); ``` Also isolate database rows, files, queues, and other state. shutdown-check runs the actual service and sends real requests. ## Resolve paths reliably By default, relative paths resolve from `process.cwd()`. Test runners usually use the project root, but this can change in workspaces or custom scripts. Pass a base directory explicitly: ```js const result = await checkShutdown(config, import.meta.dirname); ``` Then relative `cwd` values are resolved from the test file's directory. ## Avoid leaked processes after a test failure shutdown-check cleans up its workload requests, timers, and service process group before resolving. Do not start the same server in a `beforeAll` hook; the check owns the process lifecycle. If a test runner force-stops the test before shutdown-check resolves, cleanup may not finish. This is another reason to use an outer timeout longer than the check's own timeouts. ## Related - [Node API](https://shutdown.jscrate.dev/docs/node-api) - [Configuration reference](https://shutdown.jscrate.dev/docs/configuration) - [Output and reports](https://shutdown.jscrate.dev/docs/output) - [Run in CI](https://shutdown.jscrate.dev/docs/ci) - [Quick start](https://shutdown.jscrate.dev/docs/quick-start) --- # CI > Run a graceful shutdown test in CI with shutdown-check: GitHub Actions and GitLab CI examples, JUnit reports, exit codes, and a dedicated port per job. Source: https://shutdown.jscrate.dev/docs/ci Last updated: 2026-09-23 Run shutdown-check after building your application. The CLI's exit code fails the job, while JSON and JUnit preserve the diagnostic code, timeline, and captured service output. ## Prepare the project Before adding CI configuration: 1. Run `npx shutdown-check test` locally. 2. Commit `shutdown-check.json`. 3. Point `command` at the built entry file. 4. Reserve a port for this test. 5. Use isolated database, queue, and file resources. 6. Make sure the workload route is deterministic. shutdown-check starts and stops the service itself. Do not start the same service in an earlier CI step or service container on the configured port. ## GitHub Actions ```yaml title=".github/workflows/shutdown.yml" name: Graceful shutdown on: push: branches: [main] pull_request: jobs: shutdown-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 22 cache: npm - run: npm ci - run: npm run build - name: Test graceful shutdown run: npx shutdown-check test --json --junit shutdown-result.xml - name: Save shutdown report uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: shutdown-check-report path: shutdown-result.xml if-no-files-found: ignore ``` `--json` keeps the full structured result in the job log. Remove it if you prefer the readable text timeline. The artifact step uses `if: !cancelled()` so it still runs after a failed check. `if-no-files-found: ignore` handles setup errors, which exit with code `2` before a JUnit result exists. To show the result directly in the pull-request checks, pass `shutdown-result.xml` to a JUnit reporter action instead of, or in addition to, uploading it. ## GitLab CI ```yaml title=".gitlab-ci.yml" shutdown-check: image: node:22 script: - npm ci - npm run build - npx shutdown-check test --junit shutdown-result.xml artifacts: when: always reports: junit: shutdown-result.xml paths: - shutdown-result.xml ``` GitLab reads the report and adds the test case to the merge request. The artifact remains downloadable after failure because `when: always` is set. ## Exit codes in CI | Exit code | Meaning | | --- | --- | | `0` | passed | | `1` | shutdown check failed | | `2` | setup/configuration error | | Code | CI meaning | JUnit file | | ---- | ---------------------------------------------- | ---------------------- | | `0` | The shutdown behavior passed | Written when requested | | `1` | The test ran and a shutdown check failed | Written when requested | | `2` | Arguments, config, or setup prevented the test | Not written | Do not add `|| true` to the shutdown command. That hides the exit code and can let a broken deployment behavior merge. ## Give parallel jobs separate ports shutdown-check fails with [SC001](https://shutdown.jscrate.dev/docs/codes/sc001) if anything already owns `baseUrl`. Give each job or matrix entry a different port and pass it to the service through `env`. ```json title="shutdown-check.json" { "env": { "PORT": "3510" }, "baseUrl": "http://127.0.0.1:3510" } ``` For dynamic matrices, generate a small config in the job or call the Node API with a port derived from the matrix value. Do not run concurrent shutdown tests against the same database records or queue messages. ## Choose CI timeouts The job must allow enough time for: - dependency installation and build; - `readiness.timeoutMs`; - `workload.started.timeoutMs`; - `shutdown.deadlineMs`; - a small process and runner overhead. A test-runner timeout that ends first removes the useful shutdown result. Keep the outer timeout above the sum of the shutdown-check timeouts, while keeping the shutdown deadline realistic for production. ## Diagnose a failed job 1. Read the SC code in the first output line. 2. Find the last successful timeline event. 3. Read captured stderr and stdout. 4. Open the page for the diagnostic code. 5. Reproduce with the same build command and environment locally. Use [troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting) when the failure is timing-related or occurs before the service becomes ready. ## Run through an existing test suite If CI already publishes node:test or Vitest results, call `checkShutdown()` from a test file. The existing runner can report the assertion, and you can print the timeline on failure. See [tests with node:test and Vitest](https://shutdown.jscrate.dev/docs/guides/test-runners) for complete examples and timeout guidance. ## Supported runners Use Linux or macOS with Node.js 22 or later. Windows runners are not supported because shutdown-check depends on POSIX signals and process groups. A Linux container runner is supported when it can start child processes and bind a local port. ## Related - [Quick start](https://shutdown.jscrate.dev/docs/quick-start) - [Output and reports](https://shutdown.jscrate.dev/docs/output) - [CLI reference](https://shutdown.jscrate.dev/docs/cli) - [Tests with node:test and Vitest](https://shutdown.jscrate.dev/docs/guides/test-runners) - [Troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting) --- # CLI > The shutdown-check CLI reference: the init and test commands, every flag, how the config file is found, the text output and the 0, 1 and 2 exit codes. Source: https://shutdown.jscrate.dev/docs/cli Last updated: 2026-09-23 The shutdown-check CLI has two commands. `init` creates a starter config. `test` starts your service and verifies its behavior after `SIGTERM`. ## Install and open the help ```bash npm install --save-dev shutdown-check npx shutdown-check --help ``` The installed help output is: ```text shutdown-check — verify graceful shutdown with a real in-flight HTTP request Usage: shutdown-check init [--config FILE] shutdown-check test [--config FILE] [--json] [--junit FILE] shutdown-check --help shutdown-check --version Default config: shutdown-check.json Exit codes: 0 passed, 1 shutdown check failed, 2 setup/configuration error ``` Running `shutdown-check` without arguments also prints the help. ## Commands | Command | What it does | | --- | --- | | `init` | Write a starter shutdown-check.json | | `test` | Run the graceful shutdown check | ## Create a config with init ```bash npx shutdown-check init ``` `init` writes `shutdown-check.json` in the current directory. | Option | What it does | | --- | --- | | `--config FILE` | Read (for `test`) or write (for `init`) this config file instead of `shutdown-check.json`. Paths in it resolve from the file's folder. | Use `--config` to choose another path: ```bash npx shutdown-check init --config config/shutdown-check.json ``` The command prints the created path and reminds you to edit the command, port, and routes. It never overwrites a file. If the target exists, the CLI exits with code `2`: ```text shutdown-check: EEXIST: file already exists, open '/home/me/my-service/shutdown-check.json' ``` Edit the existing file, rename it, or choose a different `--config` path. The [configuration reference](https://shutdown.jscrate.dev/docs/configuration) explains every generated field. ## Run a check with test ```bash npx shutdown-check test ``` `test` reads the config, starts the command, waits for readiness, creates active work, sends `SIGTERM`, and checks the response and process exit. | Option | What it does | | --- | --- | | `--config FILE` | Read (for `test`) or write (for `init`) this config file instead of `shutdown-check.json`. Paths in it resolve from the file's folder. | | `--json` | Print the full result as JSON on stdout instead of the text timeline. | | `--junit FILE` | Also write a JUnit XML report with one test case to this file. | Common forms: ```bash npx shutdown-check test npx shutdown-check test --config config/shutdown-check.json npx shutdown-check test --json npx shutdown-check test --junit shutdown-result.xml npx shutdown-check test --json --junit shutdown-result.xml ``` ### `--config FILE` Read a config from another path. Relative paths are resolved from the current working directory. Inside the file, `cwd` is resolved from the config file's folder. ### `--json` Replace the readable text output on stdout with the complete JSON result. This is useful for scripts and structured CI logs. It does not change the process exit code. ### `--junit FILE` Write a JUnit XML report in addition to text or JSON output. The parent folder must already exist. The file is written before the normal result is printed. `--json` and `--junit` belong to `test`. Passing either to `init` is an unknown option. ## Help and version flags | Flag | Short | Behavior | | ----------- | ----- | ------------------------------------------------------- | | `--help` | `-h` | Print help; it must be the first argument | | `--version` | `-v` | Print the installed version; no other argument is valid | ```bash npx shutdown-check --version ``` ## How is the config path resolved? Without `--config`, the CLI reads `shutdown-check.json` from the directory where you run the command. With this layout: ```text my-service/ ├── config/ │ └── shutdown-check.json └── dist/ └── server.js ``` run: ```bash npx shutdown-check test --config config/shutdown-check.json ``` Then use `"cwd": ".."` inside the config if the server command should run from `my-service/`. The CLI accepts JSON only. For a TypeScript config, use `defineConfig()` and `checkShutdown()` from the [Node API](https://shutdown.jscrate.dev/docs/node-api). ## Text output The default output contains: 1. `PASS` or `FAIL`, followed by a diagnostic code and message; 2. a timeline with milliseconds since the run began; 3. on failure, the last 8 KiB of service stderr and stdout when present. ```text FAIL SC101: Service exited before becoming ready: code 1 Timeline: + 7 ms process launched — pid=74471 + 47 ms process exited — code=1, signal=none + 111 ms check failed — SC101: Service exited before becoming ready: code 1 Service stderr (last 8 KiB): Error: Cannot find module ./dist/server.js ``` Start with the code, then read the final successful timeline event. The [diagnostic code pages](https://shutdown.jscrate.dev/docs/codes) give a cause and fix for each failure. ## Errors before the test begins Command-line and config errors start with `shutdown-check:`. They have no SC code or timeline because the test did not run. | Example message | What to change | | ------------------------------------------------------------------------------------------------------------- | ------------------------------- | | `Unknown command "run". Run shutdown-check --help.` | Use `init` or `test` | | `Unknown option "--verbose". Run shutdown-check --help.` | Remove the unsupported flag | | `--config requires a file path` | Add a path after `--config` | | `--junit requires a file path` | Add a path after `--junit` | | `Cannot read JSON config /path/shutdown-check.json: Error: ENOENT: no such file or directory, open '/path/…'` | Fix the path or create the file | | `baseUrl must use http://localhost, http://127.0.0.1 or http://[::1]` | Use a supported local origin | See [troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting) for configuration, startup, and shutdown failures grouped by symptom. ## Exit codes | Exit code | Meaning | | --- | --- | | `0` | passed | | `1` | shutdown check failed | | `2` | setup/configuration error | - `0` means the configured shutdown contract passed. - `1` means the check ran and returned a failing SC code. - `2` means invalid arguments, configuration, or setup stopped the check. A code `1` run can still write JSON and JUnit. A code `2` run has no test result, so it writes no JUnit report. ## Other package managers | npm | pnpm | Yarn | Bun | | ------------------------- | ------------------------------- | -------------------------- | -------------------------- | | `npx shutdown-check test` | `pnpm exec shutdown-check test` | `yarn shutdown-check test` | `bunx shutdown-check test` | Each command runs the version installed in the current project. ## Related - [Quick start](https://shutdown.jscrate.dev/docs/quick-start) - [Configuration reference](https://shutdown.jscrate.dev/docs/configuration) - [Output and reports](https://shutdown.jscrate.dev/docs/output) - [Run in CI](https://shutdown.jscrate.dev/docs/ci) - [Troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting) --- # 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: ` 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` | `{}` | 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` | `{}` | 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) --- # Node API > The shutdown-check Node API: run a check from a test with checkShutdown, validate configs, build a custom runner with runCheck and write JUnit XML. Source: https://shutdown.jscrate.dev/docs/node-api Last updated: 2026-09-23 The shutdown-check Node API is useful when the check belongs inside a test suite or custom script. It runs the same engine as the CLI, but it returns data instead of printing output or exiting your process. ## Import the package ES modules: ```ts import { checkShutdown, defineConfig } from "shutdown-check"; ``` CommonJS, supported since 1.0.1: ```js const { checkShutdown, defineConfig } = require("shutdown-check"); ``` TypeScript declarations are included for both entry points. ## Exports | Export | Use it when | | --------------- | ------------------------------------------------------------ | | `checkShutdown` | You have an input object and want validation plus a full run | | `defineConfig` | You want type checking and completion while writing config | | `parseConfig` | You want to validate an object and apply defaults | | `loadConfig` | You want to read and validate a JSON file | | `runCheck` | You already have a validated `CheckConfig` | | `junitXml` | You want to turn a `CheckResult` into JUnit XML | The package also exports the config, result, diagnostic, and timeline types. ## Run a check with checkShutdown `checkShutdown()` is the recommended high-level function. It validates the input, fills in defaults, runs the check, performs cleanup, and resolves to a `CheckResult`. ```ts title="test/shutdown.test.ts" import assert from "node:assert/strict"; import { test } from "node:test"; import { checkShutdown } from "shutdown-check"; test("finishes active requests on SIGTERM", { timeout: 30_000 }, async () => { const result = await checkShutdown({ command: ["node", "dist/server.js"], env: { PORT: "3510" }, baseUrl: "http://127.0.0.1:3510", readiness: { path: "/health" }, workload: { path: "/slow", bodyIncludes: "work complete", started: { type: "response-headers" }, }, shutdown: { deadlineMs: 10_000, readinessWithdrawal: true, }, }); assert.equal(result.pass, true, `${result.code}: ${result.message}`); }); ``` A normal shutdown failure does not throw. It resolves with `pass: false` and a diagnostic code. Invalid config rejects with a validation `Error`. ```ts function checkShutdown(input: CheckConfigInput, baseDirectory?: string): Promise ``` Validates `input` with `parseConfig()` and runs the check. Relative paths resolve from `baseDirectory` (default `process.cwd()`). It starts and cleans up its own process, and returns `pass: false` for a failed check rather than throwing; it throws only for an invalid config. ### Resolve relative paths The second argument is the base directory used for relative `cwd` values. It defaults to `process.cwd()`. ```ts const result = await checkShutdown(config, import.meta.dirname); ``` Pass the test file's directory when the test runner may use another working directory. ## Type a config with defineConfig `defineConfig()` returns the object unchanged. It exists to provide editor completion and catch invalid types before the test runs. ```ts title="shutdown.config.ts" import { defineConfig } from "shutdown-check"; export default defineConfig({ command: ["node", "dist/server.js"], baseUrl: "http://127.0.0.1:3510", readiness: { path: "/health", timeoutMs: 30_000 }, workload: { path: "/slow", concurrent: 2, started: { type: "response-headers" }, }, shutdown: { deadlineMs: 15_000 }, }); ``` Import the object and pass it to `checkShutdown()`. The CLI does not load TypeScript config files. ```ts function defineConfig(input: T): T ``` Returns `input` unchanged. Use it for editor completion and type checking when you write a config in TypeScript. ## Validate input with parseConfig `parseConfig()` validates an unknown object and returns a complete `CheckConfig` with defaults applied. ```ts import { parseConfig } from "shutdown-check"; const config = parseConfig( { command: ["node", "dist/server.js"], baseUrl: "http://127.0.0.1:3510", readiness: { path: "/health" }, workload: { path: "/slow", started: { type: "response-headers" }, }, }, process.cwd() ); console.log(config.shutdown.deadlineMs); // 10000 ``` The optional second argument controls how relative `cwd` is resolved. Validation throws an `Error` naming the first invalid field. ## Read a JSON config with loadConfig `loadConfig()` reads a file, parses its JSON, validates it, and resolves `cwd` from the file's folder. ```ts import { loadConfig } from "shutdown-check"; const config = await loadConfig("config/shutdown-check.json"); ``` File errors, JSON syntax errors, and validation errors are thrown. The messages match the ones used by the CLI. ```ts function parseConfig(raw: unknown, baseDirectory?: string): CheckConfig ``` Validates a config object and fills in every default, resolving `cwd` from `baseDirectory` (default `process.cwd()`). Throws an `Error` naming the first invalid field. ```ts function loadConfig(file: string): Promise ``` Reads and validates a JSON config file. Relative paths in it resolve from the file's folder. ## Build a custom runner Use `runCheck()` after `parseConfig()` or `loadConfig()` when you need custom logging or report handling. ```ts title="scripts/check-shutdown.ts" import { mkdir, writeFile } from "node:fs/promises"; import { junitXml, loadConfig, runCheck } from "shutdown-check"; const config = await loadConfig("shutdown-check.json"); const result = await runCheck(config); for (const item of result.timeline) { const detail = item.detail ? ` — ${item.detail}` : ""; console.log(`+${item.ms} ms ${item.event}${detail}`); } await mkdir("reports", { recursive: true }); await writeFile( "reports/shutdown.xml", junitXml(result, "api-server shutdown") ); if (!result.pass) { console.error(`${result.code}: ${result.message}`); if (result.stderr) console.error(result.stderr); process.exitCode = 1; } ``` `runCheck()` expects a valid `CheckConfig`; it does not validate a hand-built object again. Bypassing validation can lead to [SC002](https://shutdown.jscrate.dev/docs/codes/sc002) or [SC999](https://shutdown.jscrate.dev/docs/codes/sc999). ```ts function runCheck(config: CheckConfig): Promise ``` Runs the check with an already validated config, from `parseConfig()` or `loadConfig()`. ```ts function junitXml(result: CheckResult, suiteName?: string): string ``` Turns a result into JUnit XML: one suite (`suiteName`, default `"shutdown-check"`) with one test case, "graceful shutdown". The timeline goes in `system-out`, stderr in `system-err`, and a failed check adds a `failure` element typed with its code. The optional second argument to `junitXml()` names the test suite. It defaults to `shutdown-check`. ## Understand CheckResult Every completed check resolves to the same object printed by CLI `--json`. | Option | Type | Default | Description | | --- | --- | --- | --- | | `pass` (required) | `boolean` | — | `true` only for `SC000`. | | `code` (required) | `string` | — | The [diagnostic code](https://shutdown.jscrate.dev/docs/codes): `SC000` on a pass, the first failure otherwise. | | `message` (required) | `string` | — | What happened, in one sentence, with the values observed. | | `timeline` (required) | `TimelineEvent[]` | — | Every step of the check in order, with milliseconds since it started. | | `stdout` (required) | `string` | — | The last 8 KiB the service wrote to stdout. | | `stderr` (required) | `string` | — | The last 8 KiB the service wrote to stderr. | Important fields: - `pass` is the value to assert in a test. - `code` is `SC000` on a pass or the first failure code. - `message` is the human-readable result. - `timeline` records each observed step. - `stdout` and `stderr` contain the captured service output tails. - `startedAt` and `durationMs` describe the run. Each item in `timeline` has this shape: | Option | Type | Default | Description | | --- | --- | --- | --- | | `ms` (required) | `number` | — | Milliseconds since the check started, rounded. | | `event` (required) | `string` | — | What happened: `"process launched"`, `"service ready"`, `"signal sent"`, `"work request finished"`, `"process exited"` and so on. | | `detail` | `string` | — | The values behind it, such as `"HTTP 503"` or `"code=0, signal=none"`. | The [output reference](https://shutdown.jscrate.dev/docs/output) lists every event name and its detail. ## What throws? | Situation | `checkShutdown()` | `runCheck()` | | ------------------------------- | --------------------------- | --------------------------- | | Invalid input config | Rejects with `Error` | Not validated | | Expected check failure | Resolves with `pass: false` | Resolves with `pass: false` | | Unexpected error during the run | Resolves with `SC999` | Resolves with `SC999` | Wrap calls in `try/catch` when configuration may be invalid. For an ordinary test failure, inspect the returned code and timeline. ## Cleanup and parallel tests Before resolving, the API closes its client requests, clears timers, and force-kills the service process group when necessary. A passing service has already exited and closed its port by itself. Parallel checks must use separate ports and isolated test data. Otherwise one run may fail with [SC001](https://shutdown.jscrate.dev/docs/codes/sc001) or affect another run's workload. ## Related - [Tests with node:test and Vitest](https://shutdown.jscrate.dev/docs/guides/test-runners) - [Configuration reference](https://shutdown.jscrate.dev/docs/configuration) - [Output and reports](https://shutdown.jscrate.dev/docs/output) - [Diagnostic codes](https://shutdown.jscrate.dev/docs/codes) - [CLI reference](https://shutdown.jscrate.dev/docs/cli) --- # Output and reports > What shutdown-check prints and writes: the text timeline, every timeline event, the --json result, the shutdown-check JUnit report and the exit codes. Source: https://shutdown.jscrate.dev/docs/output Last updated: 2026-09-23 Every shutdown-check run produces one result with a verdict, diagnostic code, message, timeline, duration, and captured service output. Choose text for people, JSON for scripts, or JUnit XML for CI reports. ## Choose an output format | Format | Command option | Destination | Best for | | --------- | -------------- | ----------- | ----------------------------- | | Text | default | stdout | Local debugging and CI logs | | JSON | `--json` | stdout | Scripts and structured logs | | JUnit XML | `--junit FILE` | file | CI test-report interfaces | | Exit code | always | process | Passing or failing the caller | You can write JUnit while printing either text or JSON: ```bash npx shutdown-check test --json --junit shutdown-result.xml ``` ## Read the text output A passing result contains a verdict and timeline: ```text PASS SC000: Graceful shutdown verified Timeline: + 5 ms process launched — pid=67911 + 113 ms service ready — HTTP 200 + 113 ms work request sent — #1 GET /slow + 113 ms work request sent — #2 GET /slow + 114 ms work confirmed active — 2 response(s) sent headers; bodies still in progress + 114 ms signal sent — SIGTERM + 115 ms readiness withdrawn — HTTP 503 + 115 ms new request rejected — HTTP 503 + 2117 ms work request finished — #1 HTTP 200 + 2117 ms work request finished — #2 HTTP 200 + 2120 ms process exited — code=0, signal=none + 2121 ms shutdown verified — work completed and service exited before deadline ``` The number after `+` is milliseconds since the run began. Read the lines in order to see what happened before and after `SIGTERM`. A failure ends with `check failed`: ```text FAIL SC201: In-flight request was interrupted: aborted Timeline: + 6 ms process launched — pid=73692 + 112 ms service ready — HTTP 200 + 113 ms work request sent — #1 GET /slow + 113 ms work confirmed active — 1 response(s) sent headers; bodies still in progress + 113 ms signal sent — SIGTERM + 116 ms process exited — code=null, signal=SIGTERM + 116 ms work request finished — #1 aborted + 116 ms check failed — SC201: In-flight request was interrupted: aborted ``` Open the page for the code—in this case [SC201](https://shutdown.jscrate.dev/docs/codes/sc201)—to see causes and fixes. ## Service stdout and stderr On a failed check, text output adds the last 8 KiB of each non-empty service stream: ```text Service stderr (last 8 KiB): Error: database close timed out Service stdout (last 8 KiB): SIGTERM received closing HTTP server ``` Earlier output is discarded when a stream grows beyond 8 KiB. Keep shutdown logs concise and write important state near the failure. The service output is captured, not streamed live. It appears after the result so the timeline remains readable. ## Timeline events | Event | Meaning | | ----------------------- | --------------------------------------------------------- | | `process launched` | The configured command started | | `service ready` | The readiness route returned its expected status | | `work request sent` | A workload request was opened | | `work confirmed active` | The selected start barrier passed | | `signal sent` | The first `SIGTERM` was delivered | | `signal repeated` | The optional second `SIGTERM` was delivered | | `readiness withdrawn` | The readiness route stopped reporting ready | | `new request rejected` | New work returned an allowed status or connection refusal | | `work request finished` | A workload response completed or failed | | `process exited` | The launched process stopped | | `shutdown verified` | Every configured check passed | | `check failed` | The run ended with the displayed diagnostic code | An event appears only when that part of the config runs. For example, `readiness withdrawn` is absent unless `shutdown.readinessWithdrawal` is enabled. ## JSON output Run: ```bash npx shutdown-check test --json ``` The command prints one JSON object. A shortened passing example: ```json { "pass": true, "code": "SC000", "message": "Graceful shutdown verified", "startedAt": "2026-09-23T10:00:00.000Z", "durationMs": 2121, "timeline": [ { "ms": 5, "event": "process launched", "detail": "pid=67911" }, { "ms": 114, "event": "signal sent", "detail": "SIGTERM" }, { "ms": 2121, "event": "shutdown verified", "detail": "work completed and service exited before deadline" } ], "stdout": "", "stderr": "" } ``` | Option | Type | Default | Description | | --- | --- | --- | --- | | `pass` (required) | `boolean` | — | `true` only for `SC000`. | | `code` (required) | `string` | — | The [diagnostic code](https://shutdown.jscrate.dev/docs/codes): `SC000` on a pass, the first failure otherwise. | | `message` (required) | `string` | — | What happened, in one sentence, with the values observed. | | `timeline` (required) | `TimelineEvent[]` | — | Every step of the check in order, with milliseconds since it started. | | `stdout` (required) | `string` | — | The last 8 KiB the service wrote to stdout. | | `stderr` (required) | `string` | — | The last 8 KiB the service wrote to stderr. | The `timeline` array contains: | Option | Type | Default | Description | | --- | --- | --- | --- | | `ms` (required) | `number` | — | Milliseconds since the check started, rounded. | | `event` (required) | `string` | — | What happened: `"process launched"`, `"service ready"`, `"signal sent"`, `"work request finished"`, `"process exited"` and so on. | | `detail` | `string` | — | The values behind it, such as `"HTTP 503"` or `"code=0, signal=none"`. | JSON uses the same exit codes as text. A failure still exits with `1`, so a shell step may need to preserve stdout before reacting to the exit. ## JUnit report Run: ```bash npx shutdown-check test --junit reports/shutdown.xml ``` The parent directory must exist. shutdown-check writes one test suite with one test case: - a pass has no failure element; - a failed shutdown adds the diagnostic code and message as the failure; - the timeline is included in system output; - captured service stdout and stderr are included when available; - the suite time uses the run duration. The default suite name is `shutdown-check`. From the Node API, pass a second argument to `junitXml(result, name)` to choose another name. The report is written before the CLI prints text or JSON. A normal check failure therefore still produces the file. A setup or config error produces no report because no `CheckResult` exists. ## Exit codes | Exit code | Meaning | | --- | --- | | `0` | passed | | `1` | shutdown check failed | | `2` | setup/configuration error | | Code | Meaning | Output available | | ---- | ------------------------------------------- | -------------------------------- | | `0` | The configured shutdown contract passed | Text/JSON and optional JUnit | | `1` | The test ran and returned a failing SC code | Text/JSON and optional JUnit | | `2` | Arguments, setup, or config stopped the run | Error on stderr; no JUnit result | CI should fail on any nonzero code. Treat `2` as a broken test setup and `1` as a service behavior that needs investigation. ## Use the result from code `checkShutdown()` and `runCheck()` return the same `CheckResult` object used by JSON output. `junitXml()` creates the same XML as the CLI. ```ts import { checkShutdown, junitXml } from "shutdown-check"; const result = await checkShutdown(config); const xml = junitXml(result, "orders-api shutdown"); ``` See the [Node API](https://shutdown.jscrate.dev/docs/node-api) for a complete custom runner. ## Related - [CLI reference](https://shutdown.jscrate.dev/docs/cli) - [Diagnostic codes](https://shutdown.jscrate.dev/docs/codes) - [Run in CI](https://shutdown.jscrate.dev/docs/ci) - [Node API](https://shutdown.jscrate.dev/docs/node-api) - [Troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting) --- # Compatibility and limits > shutdown-check compatibility: Node.js 22 or later on macOS and Linux, ESM and CommonJS, any HTTP/1 framework, local HTTP, SIGTERM only, and what it skips. Source: https://shutdown.jscrate.dev/docs/compatibility Last updated: 2026-09-23 shutdown-check 1.0.1 runs on Node.js 22 or later on macOS and Linux. It can test any local HTTP/1 service because it works with the service process and port instead of importing framework code. ## Support matrix | Area | Supported | | -------------------- | ---------------------------------------------------------------- | | Node.js | 22 or later | | Operating systems | macOS and Linux | | Module systems | ESM and CommonJS | | TypeScript | Declarations for both module entry points | | Frameworks | node:http, Express, Fastify, NestJS, and other HTTP/1 frameworks | | Protocol | Local plain HTTP/1 | | Hosts | `localhost`, `127.0.0.1`, and `[::1]` | | Shutdown signal | `SIGTERM` | | Reports | Text, JSON, and JUnit XML | | Runtime dependencies | None | | License | MIT | ## Node.js version The machine running shutdown-check needs Node.js 22 or later. The tool uses modern Node.js APIs and declares this requirement in its package metadata. The service is started by your configured command. In practice, it normally uses the same Node.js installation as the check. CI examples therefore select Node.js 22 explicitly. ## Operating systems Only macOS and Linux are supported. shutdown-check relies on POSIX behavior: - `SIGTERM` delivery; - detached process groups; - signaling a process group during cleanup; - local TCP connection behavior. Windows does not provide the same signal and process-group model. Run the test inside WSL, a Linux container, or a Linux CI runner instead of native Windows. ## HTTP and network limits `baseUrl` must use plain HTTP and a loopback host: ```text http://localhost:3000 http://127.0.0.1:3000 http://[::1]:3000 ``` The tool does not connect to remote hosts. It also rejects credentials, query strings, fragments, and a path in `baseUrl`. Put paths in `readiness`, `workload`, start-probe, and new-request fields. The following are outside the current protocol support: - HTTPS and TLS termination; - HTTP/2 streams; - WebSocket connections; - Unix sockets; - remote staging or production URLs. Test the local HTTP server behind a reverse proxy, not the proxy itself. ## Framework compatibility shutdown-check has no framework adapter. If the service starts from a command, listens on local HTTP, and handles `SIGTERM`, it can be tested. The framework still controls the shutdown behavior. For example: - node:http and Express use the underlying `server.close()`; - Fastify provides `fastify.close()`; - NestJS requires shutdown hooks to be enabled and awaited; - a custom framework must stop accepting work and close resources itself. Use the [Node.js graceful shutdown guide](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) for node:http, Express, and Fastify examples. ## Module support The package provides ESM and CommonJS entry points with TypeScript types. ```ts import { checkShutdown } from "shutdown-check"; ``` ```js const { checkShutdown } = require("shutdown-check"); ``` CommonJS support and the `--version` flag were added in 1.0.1. ## Signal support Only `SIGTERM` is accepted. This is the normal graceful-stop signal used by Linux process managers, container runtimes, and Kubernetes. `SIGINT`, custom signals, and Windows console events are not supported. A config with another signal is rejected before the test starts. The optional `repeatSignalAfterMs` sends `SIGTERM` a second time. It does not change the signal type. ## What the test can verify The tool can directly observe: - whether the service starts and becomes ready; - whether a request is active before the signal; - whether active responses finish with the expected status and body; - whether readiness changes; - whether new HTTP work is rejected; - whether the process exits with the expected code and deadline; - whether the HTTP port closes; - whether a wrapper leaves a child server running. ## What the test cannot see directly An HTTP response does not reveal every internal operation. shutdown-check does not directly verify: - database transactions after the response; - queue message acknowledgement; - background jobs; - file flushes; - external service cleanup; - WebSocket or HTTP/2 session draining; - container endpoint removal; - load-balancer propagation; - a Kubernetes `preStop` hook. Use `workload.bodyIncludes` when the response can prove success. Add application-specific integration tests for work that continues after the response. ## Container support The test can run inside a Linux container when: - Node.js 22 or later is installed; - the package is available; - child processes can be created and signaled; - the service binds to a loopback port inside the same container. That setup tests the server in the image, but not Kubernetes endpoint updates, `preStop`, or the cluster grace period. Test those separately in a staging cluster. See [Kubernetes and containers](https://shutdown.jscrate.dev/docs/guides/kubernetes). ## Resource and safety limits - `workload.concurrent` supports 1 to 20 requests. - Response bodies are captured up to 1 MiB. - Service stdout and stderr retain their last 8 KiB. - All timeouts have documented bounds up to 300000 ms. - Every request path must remain on the configured local origin. - Failed runs force-kill the process group during cleanup. Do not point the workload at destructive production behavior. Use isolated test data and test-only routes where appropriate. ## Related - [Configuration reference](https://shutdown.jscrate.dev/docs/configuration) - [Graceful shutdown in Node.js](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) - [Kubernetes and containers](https://shutdown.jscrate.dev/docs/guides/kubernetes) - [In-flight work](https://shutdown.jscrate.dev/docs/in-flight-work) - [About shutdown-check](https://shutdown.jscrate.dev/docs/about) --- # 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) --- # Diagnostic codes > The shutdown-check diagnostic codes: what each SC code means, the stage of the check it comes from, and what to fix, from SC000 (pass) to SC999. Source: https://shutdown.jscrate.dev/docs/codes Last updated: 2026-09-23 Every shutdown-check run ends with one diagnostic code. `SC000` is a pass. Any other code points to the first stage that failed, so you can start with the relevant part of the service instead of reading the whole timeline. ## Find a code | Code | What happened | Result | | --- | --- | --- | | [SC000](https://shutdown.jscrate.dev/docs/codes/sc000) | Graceful shutdown verified | PASS | | [SC001](https://shutdown.jscrate.dev/docs/codes/sc001) | Port already in use | FAIL | | [SC002](https://shutdown.jscrate.dev/docs/codes/sc002) | No launch command | FAIL | | [SC100](https://shutdown.jscrate.dev/docs/codes/sc100) | Service never became ready | FAIL | | [SC101](https://shutdown.jscrate.dev/docs/codes/sc101) | Service exited before it was ready | FAIL | | [SC110](https://shutdown.jscrate.dev/docs/codes/sc110) | Start probe was already active | FAIL | | [SC111](https://shutdown.jscrate.dev/docs/codes/sc111) | Work was never in flight | FAIL | | [SC112](https://shutdown.jscrate.dev/docs/codes/sc112) | Exited before SIGTERM | FAIL | | [SC113](https://shutdown.jscrate.dev/docs/codes/sc113) | SIGTERM could not be delivered | FAIL | | [SC114](https://shutdown.jscrate.dev/docs/codes/sc114) | Second SIGTERM could not be delivered | FAIL | | [SC200](https://shutdown.jscrate.dev/docs/codes/sc200) | In-flight request did not finish | FAIL | | [SC201](https://shutdown.jscrate.dev/docs/codes/sc201) | In-flight request was interrupted | FAIL | | [SC202](https://shutdown.jscrate.dev/docs/codes/sc202) | In-flight request got the wrong status | FAIL | | [SC203](https://shutdown.jscrate.dev/docs/codes/sc203) | In-flight response body was wrong | FAIL | | [SC300](https://shutdown.jscrate.dev/docs/codes/sc300) | Service did not exit in time | FAIL | | [SC301](https://shutdown.jscrate.dev/docs/codes/sc301) | Service exited with the wrong code | FAIL | | [SC302](https://shutdown.jscrate.dev/docs/codes/sc302) | Port still open after exit | FAIL | | [SC310](https://shutdown.jscrate.dev/docs/codes/sc310) | Readiness was not withdrawn | FAIL | | [SC311](https://shutdown.jscrate.dev/docs/codes/sc311) | New request was accepted during drain | FAIL | | [SC312](https://shutdown.jscrate.dev/docs/codes/sc312) | Work finished too soon to test rejection | FAIL | | [SC999](https://shutdown.jscrate.dev/docs/codes/sc999) | Unexpected error | FAIL | ## How are the codes grouped? | Range | Stage | Start here | | ------- | -------------------------- | ----------------------------------------------------------------- | | `SC000` | Passed | Review what the test covered | | `SC0xx` | Before launch | Check the port and launch command | | `SC1xx` | Startup and workload setup | Check readiness, the start barrier, and signal delivery | | `SC2xx` | Active requests | Check whether open requests finish with the expected response | | `SC30x` | Process exit | Check the deadline, exit code, and child processes | | `SC31x` | Traffic draining | Check readiness withdrawal and rejection of new work | | `SC999` | Unexpected failure | Read the original error and rule out an invalid hand-built config | Only the first failure is reported. For example, if an active request is cut off and the process also exits with the wrong code, you see the request error first. Fix it, run the test again, and then address any later failure. ## How do I read the output? Read the final timeline event before `check failed`. It shows how far the test got. Then open the page for that code and follow its fix in order. The [troubleshooting guide](https://shutdown.jscrate.dev/docs/troubleshooting) helps when the run stops before an SC code is produced. ## Related - [How the shutdown test runs](https://shutdown.jscrate.dev/docs/how-it-works) - [Output and timeline reference](https://shutdown.jscrate.dev/docs/output) - [Configuration reference](https://shutdown.jscrate.dev/docs/configuration) - [Troubleshooting by symptom](https://shutdown.jscrate.dev/docs/troubleshooting) --- # SC000: Graceful shutdown verified > SC000 (graceful shutdown verified) is the pass result: what shutdown-check proved about your service, what it does not cover, and how to keep it in CI. Source: https://shutdown.jscrate.dev/docs/codes/sc000 Last updated: 2026-09-23 `SC000` means the configured graceful shutdown behavior passed. The active requests finished, the process exited correctly before the deadline, and the port stopped accepting connections. | | | | --- | --- | | Code | `SC000` (PASS) | | Stage | Passed | | CLI exit code | 0 | | What it means | Every in-flight request finished with the expected response, and the service exited with the expected code before the deadline and closed its port. | | Message | `Graceful shutdown verified` | | First thing to check | Nothing to fix. Keep the check in CI so a regression fails the build. | ## What did the test prove? The service passed every check enabled in your config: 1. The test port was free before startup. 2. The service started and became ready. 3. The workload was active before `SIGTERM`. 4. Each active request returned the expected status and body. 5. The process exited with the expected code before the deadline. 6. The port closed after the process exited. If you enabled readiness withdrawal, rejection of new requests, or a repeated signal, those checks passed too. ## What did it not prove? The result covers only the routes and conditions in the config. It does not verify database writes, queue consumers, WebSockets, HTTP/2 streams, or work that continues after an HTTP response. It also does not test Kubernetes endpoint propagation or a container `preStop` hook. Use a workload that represents a real slow operation, and keep the test in CI so shutdown regressions fail before deployment. ## How do I keep the result meaningful? Review the config when routes, startup commands, or platform deadlines change. A test can keep passing while no longer representing production if the workload becomes much faster or the deployed entry point changes. In CI, keep the full timeline or JUnit report. A normal passing sequence shows work active before `SIGTERM`, response completion after it, and process exit before `shutdown verified`. ## Related - [Understand each stage](https://shutdown.jscrate.dev/docs/how-it-works) - [Choose a realistic workload](https://shutdown.jscrate.dev/docs/in-flight-work) - [Run the test in CI](https://shutdown.jscrate.dev/docs/ci) - [Compatibility and limits](https://shutdown.jscrate.dev/docs/compatibility) --- # SC001: Port already in use > SC001 (port already in use) means something answered on the configured port before shutdown-check launched your service. Find what holds it and fix it. Source: https://shutdown.jscrate.dev/docs/codes/sc001 Last updated: 2026-09-23 `SC001` means the port in `baseUrl` was not free before startup. shutdown-check stops because it cannot separate your service from the process that is already listening. | | | | --- | --- | | Code | `SC001` (FAIL) | | Stage | Before launch | | CLI exit code | 1 | | What it means | Something already answered on the configured port before the service was launched, so the check could not tell your service apart from it. | | Message | `The local port at is already occupied or cannot be confirmed free; use a dedicated port` | | First thing to check | Give the test a dedicated port, and stop any dev server or earlier run still listening on it. | ## Why does it happen? - A development server is still running. - Another test uses the same port. - Parallel CI jobs share one `baseUrl`. - The configured port belongs to a database, proxy, or service container. - A previous run left a child process alive. ## How do I fix it? 1. Stop the process that owns the port. 2. Give shutdown-check a port reserved for this test. 3. Pass the same port to the service through `env`. 4. Give parallel tests different ports. ```json title="shutdown-check.json" { "env": { "PORT": "3510" }, "baseUrl": "http://127.0.0.1:3510" } ``` If the problem appears after a previous test, check for a launcher that exits without stopping its child server. That later failure is reported as [SC302](https://shutdown.jscrate.dev/docs/codes/sc302). ## How do I verify the fix? Run the same command again. The first timeline event should now be `process launched`, not `check failed — SC001`. If the service then fails to become ready, continue with SC100 or SC101; the port conflict itself is gone. When the failure occurs only in CI, inspect parallel jobs and service containers. A locally free port can still be shared on the runner. ## Related - [Configure `baseUrl` and `env`](https://shutdown.jscrate.dev/docs/configuration#baseurl) - [Give tests separate ports](https://shutdown.jscrate.dev/docs/guides/test-runners#give-every-test-its-own-port) - [SC302: Port still open after exit](https://shutdown.jscrate.dev/docs/codes/sc302) --- # SC002: No launch command > SC002 (no launch command) means runCheck got a config with an empty command array. Why the CLI shows a config error instead, and how to fix it. Source: https://shutdown.jscrate.dev/docs/codes/sc002 Last updated: 2026-09-23 `SC002` means `runCheck()` received a config with no launch command. There is nothing to start, so the run ends before a process is created. | | | | --- | --- | | Code | `SC002` (FAIL) | | Stage | Before launch | | CLI exit code | 1 | | What it means | The configuration has no command to start the service. | | Message | `No launch command configured` | | First thing to check | Set `command` to an argument array, for example `["node", "server.js"]`. | ## Why do I see this code only with `runCheck()`? The CLI and `checkShutdown()` validate input first. They reject an empty `command` as a config error and exit or throw before the runner starts. `runCheck()` accepts an already validated `CheckConfig`, so a hand-built object can reach this code. ## How do I fix it? Validate the input with `parseConfig()` or `loadConfig()` before calling `runCheck()`: ```ts import { parseConfig, runCheck } from "shutdown-check"; const config = parseConfig({ command: ["node", "dist/server.js"], baseUrl: "http://127.0.0.1:3000", readiness: { path: "/health" }, workload: { path: "/slow", started: { type: "response-headers" } }, }); const result = await runCheck(config); ``` For most scripts and tests, call `checkShutdown()` instead. It validates the object and runs the check in one step. ## How do I verify the fix? Log or inspect the parsed config and confirm `command[0]` is the executable. The next run should create a `process launched` event. If the executable or entry file is wrong, the run moves to SC101 and captured stderr explains why. Do not cast an arbitrary object to `CheckConfig` to silence TypeScript. That removes the type error without adding runtime validation. ## Related - [Node API](https://shutdown.jscrate.dev/docs/node-api) - [Configure the launch command](https://shutdown.jscrate.dev/docs/configuration#command-cwd-and-env) - [SC101: Service exited before ready](https://shutdown.jscrate.dev/docs/codes/sc101) --- # SC100: Service never became ready > SC100 (service never became ready) means the readiness route did not return its expected status before readiness.timeoutMs. The causes and how to fix each. Source: https://shutdown.jscrate.dev/docs/codes/sc100 Last updated: 2026-09-23 `SC100` means the process stayed alive, but the readiness route did not return the expected status before `readiness.timeoutMs` expired. No workload request or shutdown signal was sent. | | | | --- | --- | | Code | `SC100` (FAIL) | | Stage | Startup and in-flight work | | CLI exit code | 1 | | What it means | The readiness route did not return the expected status before `readiness.timeoutMs` ran out. | | Message | `Service did not return HTTP at within ms` | | First thing to check | Check the command, the port in `baseUrl`, the readiness path and status, and the service's stderr. | ## What should I check? 1. Confirm that `baseUrl` uses the same host and port as the service. 2. Open `readiness.path` and check its real status code. 3. Make sure the service binds to `127.0.0.1`, `localhost`, or `::1`. 4. Read the captured stdout and stderr for startup warnings. 5. Increase `readiness.timeoutMs` only when startup is genuinely slow. ```json title="shutdown-check.json" { "baseUrl": "http://127.0.0.1:3000", "readiness": { "path": "/health", "status": 200, "timeoutMs": 30000 } } ``` A refused connection is normal while the service starts; shutdown-check keeps polling. If the process exits during that wait, the result is [SC101](https://shutdown.jscrate.dev/docs/codes/sc101) instead. ## How do I verify the fix? Start the service with the same command and request the exact readiness URL. It must return the configured status without authentication or redirects. After the fix, the timeline contains `service ready — HTTP 200` followed by a workload event. If readiness takes close to the full timeout, investigate slow startup before merely increasing the limit again. ## Related - [Readiness configuration](https://shutdown.jscrate.dev/docs/configuration#readiness) - [SC101: Service exited before ready](https://shutdown.jscrate.dev/docs/codes/sc101) - [Troubleshoot startup](https://shutdown.jscrate.dev/docs/troubleshooting) --- # SC101: Service exited before it was ready > SC101 (service exited before it was ready) means your command crashed, exited or failed to start before readiness passed. Read stderr, then fix the command. Source: https://shutdown.jscrate.dev/docs/codes/sc101 Last updated: 2026-09-23 `SC101` means the service exited or failed to start before readiness passed. The check did not open the workload or send `SIGTERM`. | | | | --- | --- | | Code | `SC101` (FAIL) | | Stage | Startup and in-flight work | | CLI exit code | 1 | | What it means | The launched process exited, or failed to start, before the readiness route answered. | | Messages | `Service exited before becoming ready: `
`Service exited before becoming ready` | | First thing to check | Read the captured stderr: a missing build, a bad path in `command`, a missing environment variable or a crash on boot. | ## Where do I find the cause? Start with `Service stderr` at the end of the output. Common messages include a missing build file, an invalid environment variable, a syntax error, or an address that is already in use. Also check: - `command` points to an existing executable and entry file. - `cwd` is the directory the command expects. - The application was built before the test. - Required environment variables are present in `env`. - The process does not start in the background and let its launcher exit. Prefer a direct command such as `["node", "dist/server.js"]`. Package-manager scripts and shell wrappers can hide the real exit or fail to forward signals. ## What if the process stays alive but never becomes ready? That is [SC100](https://shutdown.jscrate.dev/docs/codes/sc100). Check the configured URL, readiness path, expected status, and startup timeout. ## How do I verify the fix? Run the configured command from the configured `cwd` and confirm that it stays alive. Then run shutdown-check and look for both `process launched` and `service ready`. If the command works in your shell but not in the test, compare environment variables and relative paths. shutdown-check does not load shell aliases or shell startup files because it launches the executable directly. ## Related - [Configure `command`, `cwd`, and `env`](https://shutdown.jscrate.dev/docs/configuration#command-cwd-and-env) - [SC100: Service never became ready](https://shutdown.jscrate.dev/docs/codes/sc100) - [Troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting) --- # SC110: Start probe was already active > SC110 (start probe was already active) means the probe start barrier did not return inactiveStatus before work began. Fix the probe route or its statuses. Source: https://shutdown.jscrate.dev/docs/codes/sc110 Last updated: 2026-09-23 `SC110` means the probe reported active work before shutdown-check sent the workload request. Because the starting state is wrong, the probe cannot prove that this test caused the work to begin. | | | | --- | --- | | Code | `SC110` (FAIL) | | Stage | Startup and in-flight work | | CLI exit code | 1 | | What it means | The work-start probe did not return its inactive status before any work was sent, so it cannot prove when work begins. | | Message | `Work-start probe must return HTTP before work begins; received ` | | First thing to check | Make the probe return `inactiveStatus` while nothing is running, and a different `activeStatus` while work is in progress. | ## Why does it happen? - The probe always returns `activeStatus`. - Work from an earlier request is still running. - `activeStatus` and `inactiveStatus` are reversed. - The probe timed out or could not be reached. - The probe tracks a global state shared by parallel tests. ## How do I fix it? The route must return `inactiveStatus` before the workload starts and `activeStatus` only while that workload is running. ```json title="shutdown-check.json" { "started": { "type": "probe", "path": "/test/work-active", "inactiveStatus": 204, "activeStatus": 200 } } ``` Open the probe before running shutdown-check. If it does not return `204`, fix the route or wait for old work to finish. Keep the route test-only when it exposes internal application state. ## How do I verify the probe? Check it in both states: 1. Before work, it returns `inactiveStatus`. 2. While the workload is deliberately paused, it returns `activeStatus`. 3. After the work ends, it returns `inactiveStatus` again. The next shutdown-check run should record `work confirmed active` instead of ending immediately after `service ready`. ## Related - [Use a probe start barrier](https://shutdown.jscrate.dev/docs/in-flight-work#use-the-probe-barrier) - [Start-barrier configuration](https://shutdown.jscrate.dev/docs/configuration#start-barrier) - [SC111: Work was never in flight](https://shutdown.jscrate.dev/docs/codes/sc111) --- # SC111: Work was never in flight > SC111 (work was never in flight) means the workload finished, or never showed it started, before SIGTERM. Use a slow streaming endpoint or a probe barrier. Source: https://shutdown.jscrate.dev/docs/codes/sc111 Last updated: 2026-09-23 `SC111` means shutdown-check could not prove that the workload was active. It does not send `SIGTERM` in this state because a completed request cannot test graceful draining. | | | | --- | --- | | Code | `SC111` (FAIL) | | Stage | Startup and in-flight work | | CLI exit code | 1 | | What it means | The workload finished, or never showed that it had started, before the signal could be sent, so nothing was in flight to drain. | | Messages | `Every work request must remain active after response headers; use a slow streaming test endpoint or a probe barrier with one request`
`Work completed before its start probe became active`
`Work-start probe did not become HTTP while work remained active` | | First thing to check | Use an endpoint that stays open while it works, or a `probe` start barrier that reports the operation has begun. | ## Which barrier are you using? With `response-headers`, every request must receive headers while its body is still open. Flush the headers early and finish the body later: ```js response.writeHead(200, { "content-type": "text/plain" }); response.flushHeaders(); setTimeout(() => response.end("work complete\n"), 2000); ``` With `probe`, the separate route must change from `inactiveStatus` to `activeStatus` before the workload response finishes. ## How do I fix it? 1. Choose the barrier that matches how the handler responds. 2. Make the test workload last longer than startup and signal delivery. 3. Raise `started.timeoutMs` only if the work really takes longer to begin. 4. For concurrent requests, use `response-headers`; a probe supports one. A fast health route is not a useful workload. Use a controlled slow endpoint that represents real work without changing production data. ## How do I verify the fix? Look for this order in the timeline: ```text work request sent work confirmed active signal sent ``` If `work request finished` appears before `work confirmed active`, the route is still too fast or the barrier is observing the wrong state. ## Related - [Test in-flight work](https://shutdown.jscrate.dev/docs/in-flight-work) - [Start-barrier configuration](https://shutdown.jscrate.dev/docs/configuration#start-barrier) - [SC110: Probe already active](https://shutdown.jscrate.dev/docs/codes/sc110) - [SC112: Work ended before SIGTERM](https://shutdown.jscrate.dev/docs/codes/sc112) --- # SC112: Exited before SIGTERM > SC112 (exited before SIGTERM) means the service or a work request finished in the gap between the start barrier and the signal. Make the workload run longer. Source: https://shutdown.jscrate.dev/docs/codes/sc112 Last updated: 2026-09-23 `SC112` means the process or workload ended after the start barrier passed but before shutdown-check sent `SIGTERM`. The gap is short, so the test workload is usually only barely long enough. | | | | --- | --- | | Code | `SC112` (FAIL) | | Stage | Startup and in-flight work | | CLI exit code | 1 | | What it means | The service or a workload request finished in the moment before the signal was sent. | | Message | `Service or work exited before SIGTERM could be sent` | | First thing to check | Make the test workload slower than the time it takes to start it, so it is still running when SIGTERM arrives. | ## How do I fix it? Make the workload clearly longer than the time needed to confirm its start and send the signal. A controlled delay of one or two seconds is usually enough. Also check that: - the service is not designed to exit after one request; - the probe does not report active work too late; - a timer or background task is not stopping the process; - the test route behaves consistently on a busy CI runner. Do not solve this by increasing `shutdown.deadlineMs`; that timer starts after the signal and does not affect this gap. ## How do I verify the fix? The corrected timeline shows `signal sent` immediately after `work confirmed active`, with no process-exit or request-finished event between them. Run it several times on the slowest CI environment to make sure the timing margin is reliable. If the service itself exits in this gap, inspect application logs for a timer, one-shot mode, or unhandled error rather than extending the workload. ## Related - [Choose a reliable start barrier](https://shutdown.jscrate.dev/docs/in-flight-work) - [SC111: Work was never in flight](https://shutdown.jscrate.dev/docs/codes/sc111) - [SC113: SIGTERM could not be delivered](https://shutdown.jscrate.dev/docs/codes/sc113) --- # SC113: SIGTERM could not be delivered > SC113 (SIGTERM could not be delivered) means the operating system refused the signal to your service process. Why it happens and how to run the check. Source: https://shutdown.jscrate.dev/docs/codes/sc113 Last updated: 2026-09-23 `SC113` means the workload was active, but the operating system did not deliver `SIGTERM` to the process shutdown-check started. The shutdown behavior was not tested. | | | | --- | --- | | Code | `SC113` (FAIL) | | Stage | Startup and in-flight work | | CLI exit code | 1 | | What it means | The operating system refused to deliver SIGTERM to the process. | | Message | `Could not deliver SIGTERM to the service process` | | First thing to check | Check that the service runs as the same user as the check and has not already exited. | ## Why does it happen? This failure is uncommon because the check launches and signals the process as the same user. The usual causes are: - the process exited at the same moment the signal was sent; - a restricted container or sandbox blocks signaling; - the process ID is no longer valid; - an operating-system error prevented the signal. ## What should I do? Run the test again once. If it repeats, use a direct launch command and run the check in a normal macOS or Linux process environment. Read the preceding timeline lines to see whether the process exited first. If a wrapper receives the signal but the real server does not, the signal was still delivered; expect an exit or open-port code such as `SC301`, `SC302`, or `SC300` instead. ## How do I verify the fix? The next run should contain `signal sent — SIGTERM`. If the run then fails, the new code describes the server's shutdown behavior rather than signal delivery. When the failure happens only inside a locked-down container, compare its process and security settings with a normal local Linux run. ## Related - [How signal delivery works](https://shutdown.jscrate.dev/docs/how-it-works#6-send-sigterm) - [Compatibility and limits](https://shutdown.jscrate.dev/docs/compatibility) - [SC112: Exited before SIGTERM](https://shutdown.jscrate.dev/docs/codes/sc112) - [SC301: Wrong exit code](https://shutdown.jscrate.dev/docs/codes/sc301) --- # SC114: Second SIGTERM could not be delivered > SC114 (second SIGTERM could not be delivered) means work had already ended when repeatSignalAfterMs fired. Lower the delay or make the workload run longer. Source: https://shutdown.jscrate.dev/docs/codes/sc114 Last updated: 2026-09-23 `SC114` means shutdown-check could not send the second `SIGTERM` while the original workload was still active. The process or request ended first, or the operating system refused the signal. | | | | --- | --- | | Code | `SC114` (FAIL) | | Stage | Startup and in-flight work | | CLI exit code | 1 | | What it means | With `repeatSignalAfterMs` set, the second signal could not be sent while work was still active. | | Message | `Second SIGTERM could not be delivered while work was still active` | | First thing to check | Make the workload run longer than `repeatSignalAfterMs`, or lower that delay. | ## How do I fix it? - Set `repeatSignalAfterMs` well below the workload duration. - Make the controlled test workload stay active longer. - Keep `repeatSignalAfterMs` below `shutdown.deadlineMs`. - Check whether the first signal makes the process exit immediately. ```json title="shutdown-check.json" { "shutdown": { "deadlineMs": 10000, "repeatSignalAfterMs": 500 } } ``` The repeated signal check is optional. Use it when your process manager or operators may send `SIGTERM` more than once. A correct handler should not cut off active work when the second signal arrives. ## How do I verify the fix? A passing timeline contains `signal repeated — SIGTERM` before every `work request finished` event. The requests must then finish normally and the process must exit with the expected code. If the second signal is delivered but the request is interrupted, the result moves to SC201. Make the handler idempotent instead of removing the repeated signal check. ## Related - [Shutdown configuration](https://shutdown.jscrate.dev/docs/configuration#shutdown) - [Write an idempotent SIGTERM handler](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) - [SC201: Request interrupted](https://shutdown.jscrate.dev/docs/codes/sc201) --- # SC200: In-flight request did not finish > SC200 (in-flight request did not finish) means a request running at SIGTERM did not complete before the shutdown deadline. The causes and how to fix them. Source: https://shutdown.jscrate.dev/docs/codes/sc200 Last updated: 2026-09-23 `SC200` means an active request was still open when the shutdown deadline ended. It did not complete and it did not fail; the connection stayed open. | | | | --- | --- | | Code | `SC200` (FAIL) | | Stage | In-flight responses | | CLI exit code | 1 | | What it means | A request that was running when SIGTERM arrived did not complete before the shutdown deadline. | | Message | `In-flight request # did not finish within ms` | | First thing to check | Stop accepting connections with `server.close()` and let open requests finish; do not wait on keep-alive sockets or timers that never end. | ## Why does a request hang? - The shutdown handler waits for the request, but the request waits for shutdown cleanup. - A database call, upstream request, or lock never finishes. - The server stops reading or writing without closing the response. - `shutdown.deadlineMs` is shorter than the expected operation. - A timer or stream is left open. ## How do I fix it? 1. Add logs around the slow handler and each shutdown step. 2. Let active handlers finish before closing their dependencies. 3. Put timeouts on network, database, and queue operations. 4. Set `deadlineMs` above the slowest valid request, but below the platform's termination limit. Do not call `process.exit()` to clear this code. That cuts the connection and turns the failure into [SC201](https://shutdown.jscrate.dev/docs/codes/sc201). ## How do I verify the fix? The timeline should show `work request finished` before the deadline. Add `bodyIncludes` when a normal-looking response could still hide incomplete work. Test with the slowest valid operation, not only a fixed sleep. If external systems can stall, verify their own timeout path as a separate failure test. ## Related - [Write a graceful shutdown handler](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) - [Set the shutdown deadline](https://shutdown.jscrate.dev/docs/configuration#shutdown) - [SC201: Request interrupted](https://shutdown.jscrate.dev/docs/codes/sc201) - [SC300: Service did not exit](https://shutdown.jscrate.dev/docs/codes/sc300) --- # SC201: In-flight request was interrupted > SC201 (in-flight request was interrupted) means a request running at SIGTERM was cut off before its response ended. Stop exiting or closing sockets early. Source: https://shutdown.jscrate.dev/docs/codes/sc201 Last updated: 2026-09-23 `SC201` means an active request lost its connection after `SIGTERM`. Users often see the same failure as a reset connection or a `502` during deployment. | | | | --- | --- | | Code | `SC201` (FAIL) | | Stage | In-flight responses | | CLI exit code | 1 | | What it means | A request that was running when SIGTERM arrived was cut off: the connection was reset or closed before the response ended. | | Message | `In-flight request was interrupted: ` | | First thing to check | Do not call `process.exit()`, `closeAllConnections()` or destroy sockets while requests are still being answered. | ## What usually cuts the request off? - There is no `SIGTERM` handler, so Node.js exits immediately. - The handler calls `process.exit()` before requests finish. - Code destroys all sockets, including active ones. - A wrapper process dies and takes the server with it. - A second signal forces an early exit. ## How do I fix it? Use `server.close()` to stop new connections and wait for its callback before finishing cleanup. Do not destroy active sockets. ```js process.on("SIGTERM", () => { server.close((error) => { if (error) process.exitCode = 1; }); }); ``` If you track idle keep-alive connections, close only the idle connections. Active responses must be allowed to end normally. ## How do I verify the fix? The request should now end with `HTTP ` instead of `aborted`, `ECONNRESET`, or another socket error. The process must exit only after the request-finished event. Run the test with `repeatSignalAfterMs` when operators or process managers may send a second signal. This catches an early-exit branch that a single signal does not reach. ## Related - [Graceful shutdown in Node.js](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) - [SC200: Request did not finish](https://shutdown.jscrate.dev/docs/codes/sc200) - [SC202: Wrong response status](https://shutdown.jscrate.dev/docs/codes/sc202) - [Readiness and draining](https://shutdown.jscrate.dev/docs/guides/readiness-and-draining) --- # SC202: In-flight request got the wrong status > SC202 (in-flight request got the wrong status) means a request running at SIGTERM finished with a status other than workload.status. Find what changed it. Source: https://shutdown.jscrate.dev/docs/codes/sc202 Last updated: 2026-09-23 `SC202` means an active request finished, but its HTTP status did not match `workload.status`. The connection drained, yet the response changed during shutdown or the config expects the wrong status. | | | | --- | --- | | Code | `SC202` (FAIL) | | Stage | In-flight responses | | CLI exit code | 1 | | What it means | A request that was running when SIGTERM arrived finished, but with a different HTTP status than `workload.status`. | | Message | `In-flight request returned HTTP ; expected ` | | First thing to check | Let requests already in progress finish normally; reject only new work during shutdown. | ## What should I check? - Run the workload without shutdown and record its normal status. - Confirm that `workload.status` matches that response. - Check whether shutdown middleware returns `503` for every request. - Make sure only **new** work is rejected during the drain. - With a probe barrier, check errors that occur after the work starts. ```json title="shutdown-check.json" { "workload": { "path": "/slow", "status": 200, "started": { "type": "response-headers" } } } ``` Do not change the expected status merely to make the test pass. It should describe the response a successful request returns during normal operation. ## How do I verify the fix? Run the workload without a shutdown and during a shutdown. Both responses should use the same success status when the request began before `SIGTERM`. New requests may correctly receive `503`; the distinction is when they enter the application. Log the draining state at request start if middleware order is unclear. ## Related - [Workload configuration](https://shutdown.jscrate.dev/docs/configuration#workload) - [Reject only new traffic](https://shutdown.jscrate.dev/docs/guides/readiness-and-draining) - [SC201: Request interrupted](https://shutdown.jscrate.dev/docs/codes/sc201) - [SC203: Wrong response body](https://shutdown.jscrate.dev/docs/codes/sc203) --- # SC203: In-flight response body was wrong > SC203 (in-flight response body was wrong) means a request running at SIGTERM finished without the workload.bodyIncludes text. Check the handler and config. Source: https://shutdown.jscrate.dev/docs/codes/sc203 Last updated: 2026-09-23 `SC203` means the response returned the expected status but did not contain the text in `workload.bodyIncludes`. The request ended normally, but the result does not prove that the expected work completed. | | | | --- | --- | | Code | `SC203` (FAIL) | | Stage | In-flight responses | | CLI exit code | 1 | | What it means | A request that was running when SIGTERM arrived finished, but its body did not contain `workload.bodyIncludes`. | | Message | `In-flight response did not contain ` | | First thing to check | Check that the handler writes its full response during shutdown, and that `bodyIncludes` matches what it sends. | ## Why does it happen? - The handler returned an error page with a success status. - Shutdown skipped part of the operation. - The response text changed but the config did not. - The comparison uses different capitalization or whitespace. - The expected text appears after the 1 MiB response capture limit. ## How do I fix it? Run the route normally and choose a short, stable piece of text that appears only after success: ```json title="shutdown-check.json" { "workload": { "path": "/reports/export", "status": 200, "bodyIncludes": "export complete", "started": { "type": "probe", "path": "/test/work-active" } } } ``` The match is case-sensitive. Avoid IDs, timestamps, or other values that change on every run. ## How do I verify the fix? Inspect the real response body and confirm the marker appears after successful work. The next run should complete with the expected status and no SC203. If the response is larger than 1 MiB, choose an earlier marker or test the result through a smaller purpose-built endpoint. The captured prefix is the part shutdown-check can inspect. ## Related - [Prove the work completed](https://shutdown.jscrate.dev/docs/in-flight-work#prove-that-the-operation-completed) - [Workload configuration](https://shutdown.jscrate.dev/docs/configuration#workload) - [SC202: Wrong response status](https://shutdown.jscrate.dev/docs/codes/sc202) --- # SC300: Service did not exit in time > SC300 (service did not exit in time) means the process was still running at the shutdown deadline. Find the open handle keeping Node alive and close it. Source: https://shutdown.jscrate.dev/docs/codes/sc300 Last updated: 2026-09-23 `SC300` means every tested request finished, but the service process was still running when `shutdown.deadlineMs` ended. Something in the application remains open after the HTTP drain. | | | | --- | --- | | Code | `SC300` (FAIL) | | Stage | Process exit | | CLI exit code | 1 | | What it means | The process was still running when the shutdown deadline passed. | | Message | `Service did not exit within ms` | | First thing to check | Close the server, database pools and timers, and let the event loop empty, or exit explicitly once cleanup is done. | ## What keeps Node.js running? - The HTTP server was never closed. - A database pool or queue consumer is still connected. - An interval, watcher, or background worker is active. - Cleanup is waiting on an operation with no timeout. - A package-manager or shell wrapper is still running. ## How do I fix it? Log the start and end of every shutdown step. Close resources in an order that does not block active requests, and add timeouts to external cleanup. Let the event loop empty naturally; avoid `process.exit()` because it can interrupt pending output and requests. Raise `deadlineMs` only when the cleanup time is expected and still fits below the platform's termination limit. ## How do I find the open handle? Add a log before and after each close operation. In a development-only run, inspect active handles or temporarily remove cleanup steps until the process exits, then restore them one by one. After the fix, `process exited — code=0, signal=none` appears before the deadline. If the process exits but the port remains open, continue with SC302. ## Related - [Graceful shutdown cleanup](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) - [Shutdown deadline](https://shutdown.jscrate.dev/docs/configuration#shutdown) - [SC200: Request did not finish](https://shutdown.jscrate.dev/docs/codes/sc200) - [SC302: Port stayed open](https://shutdown.jscrate.dev/docs/codes/sc302) --- # SC301: Service exited with the wrong code > SC301 (service exited with the wrong code) means the process exited with a code other than shutdown.exitCode, or was killed by a signal. How to fix both. Source: https://shutdown.jscrate.dev/docs/codes/sc301 Last updated: 2026-09-23 `SC301` means the service stopped in time, but its exit did not match `shutdown.exitCode`. It may have returned a nonzero code or ended directly from a signal. | | | | --- | --- | | Code | `SC301` (FAIL) | | Stage | Process exit | | CLI exit code | 1 | | What it means | The process exited, but with a different exit code than `shutdown.exitCode`, or was killed by a signal. | | Messages | `Service process failed: `
`Service exited with code and signal ; expected code ` | | First thing to check | Handle SIGTERM yourself so Node does not exit with the signal, and check what sets `process.exitCode` during cleanup. | ## What does the timeline tell me? - `code=1, signal=none` means application code set a failure exit code. - `code=null, signal=SIGTERM` usually means there is no effective signal handler. - Another signal name points to a crash or an external kill. ## How do I fix it? Install a `SIGTERM` handler on the process that actually runs the server. Set `process.exitCode` only when cleanup fails, and inspect stderr for the original error. If a nonzero exit is intentional, set `shutdown.exitCode` to that value. Most services should still exit with `0` after a planned shutdown because process managers treat a nonzero code as a crash. ## How do I verify the fix? The timeline should show the configured code and `signal=none`. Also check the service logs: a clean code with a hidden cleanup error is not a healthy shutdown. If `signal=SIGTERM` remains, confirm that the listener is installed on the actual server process and not only on a parent wrapper. ## Related - [Handle SIGTERM in Node.js](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs) - [Exit-code configuration](https://shutdown.jscrate.dev/docs/configuration#shutdown) - [SC300: Service did not exit](https://shutdown.jscrate.dev/docs/codes/sc300) - [SC302: Port stayed open](https://shutdown.jscrate.dev/docs/codes/sc302) --- # SC302: Port still open after exit > SC302 (port still open after exit) means the launched process exited but the HTTP port still accepted connections: a child server kept running. How to fix it. Source: https://shutdown.jscrate.dev/docs/codes/sc302 Last updated: 2026-09-23 `SC302` means the process shutdown-check launched exited, but another process still owned the HTTP port. The usual cause is a shell or package-manager wrapper that starts the real server as a child and does not forward `SIGTERM`. | | | | --- | --- | | Code | `SC302` (FAIL) | | Stage | Process exit | | CLI exit code | 1 | | What it means | The launched process exited, but the HTTP port still accepted connections: a child server was left running. | | Message | `The launch process exited, but the HTTP port is still open or did not clearly refuse a connection (possible child process left running)` | | First thing to check | Launch the server directly, or make sure the wrapper or process manager that starts it forwards SIGTERM to the real server and waits for it. | ## How do I fix it? Launch the server process directly: ```json title="shutdown-check.json" { "command": ["node", "dist/server.js"] } ``` Avoid commands that create an extra process layer, such as `npm start`, `sh -c`, or a script that backgrounds the server. If a wrapper is required, make it replace itself with the server or forward signals and wait for the child to exit. The test cleans up the entire process group after reporting the failure, so the child should not remain after shutdown-check finishes. ## How do I verify the fix? Use the new direct command and run again. A passing run ends with process exit, a refused connection on the port, and `shutdown verified`. If a wrapper is mandatory, test it as the command. Do not test `node` directly while deploying a different signal path; that would miss the original bug. ## Related - [Configure a direct launch command](https://shutdown.jscrate.dev/docs/configuration#command-cwd-and-env) - [Containers and PID 1](https://shutdown.jscrate.dev/docs/guides/kubernetes#why-does-pid-1-matter) - [SC300: Service did not exit](https://shutdown.jscrate.dev/docs/codes/sc300) - [SC301: Wrong exit code](https://shutdown.jscrate.dev/docs/codes/sc301) --- # SC310: Readiness was not withdrawn > SC310 (readiness was not withdrawn) means the readiness route still reported ready after SIGTERM, up to the deadline. Return 503 or stop listening on SIGTERM. Source: https://shutdown.jscrate.dev/docs/codes/sc310 Last updated: 2026-09-23 `SC310` means the readiness route continued to report ready after `SIGTERM`. A load balancer may keep sending traffic to an instance that is already shutting down. | | | | --- | --- | | Code | `SC310` (FAIL) | | Stage | Draining traffic | | CLI exit code | 1 | | What it means | With `readinessWithdrawal` on, the readiness route kept returning its ready status after SIGTERM until the deadline. | | Message | `Readiness was not withdrawn before the shutdown deadline` | | First thing to check | On SIGTERM, make the readiness route return 503 (or stop listening) so load balancers stop sending traffic. | ## How do I withdraw readiness? Set a draining flag as soon as the signal arrives and return a non-ready status from the readiness route: ```js let draining = false; app.get("/health", (_request, response) => { response.sendStatus(draining ? 503 : 200); }); process.on("SIGTERM", () => { draining = true; server.close(); }); ``` Closing the listener also counts because the readiness connection is refused. A timed-out readiness request does not count as withdrawal; it leaves the traffic state unclear. ## How do I verify the fix? The timeline should record `readiness withdrawn` soon after `signal sent`. Test the route manually during a controlled drain and confirm it changes from the ready status to `503` or another non-ready status. If the application changes state but the test does not see it, check that `readiness.path` points to the same route used by the platform. ## Related - [Readiness and draining](https://shutdown.jscrate.dev/docs/guides/readiness-and-draining) - [Enable readiness withdrawal](https://shutdown.jscrate.dev/docs/configuration#shutdown) - [SC311: New request accepted](https://shutdown.jscrate.dev/docs/codes/sc311) - [Kubernetes shutdown](https://shutdown.jscrate.dev/docs/guides/kubernetes) --- # SC311: New request was accepted during drain > SC311 (new request was accepted during drain) means a new request sent after readiness was withdrawn was not rejected. Return 503 or stop listening. Source: https://shutdown.jscrate.dev/docs/codes/sc311 Last updated: 2026-09-23 `SC311` means the service accepted new work after readiness was withdrawn and while an older request was still draining. That new work may be cut off when the process exits. | | | | --- | --- | | Code | `SC311` (FAIL) | | Stage | Draining traffic | | CLI exit code | 1 | | What it means | A new request sent after readiness was withdrawn got a normal response instead of being rejected. | | Message | `New request was not rejected during drain: ` | | First thing to check | Reject new work during shutdown with a status in `rejectStatuses` (503 by default), or stop accepting connections. | ## How should the service reject new work? Choose one behavior: - Keep listening during a short drain window and return `503` for new work. - Close the listener and allow the connection to be refused. Then make `newRequests.rejectStatuses` and `newRequests.allowConnectionRefused` match that choice. ```json title="shutdown-check.json" { "shutdown": { "readinessWithdrawal": true, "newRequests": { "path": "/test/new-work", "rejectStatuses": [503], "allowConnectionRefused": true } } } ``` A timeout is not a rejection. The service must return a configured status or clearly refuse the connection. ## How do I verify the fix? The timeline should show `new request rejected — HTTP 503` or a refused connection after `readiness withdrawn` and before the original request finishes. Keep the rejection route safe and representative. A special route that always returns `503` can make the test pass without proving that real new work is blocked. ## Related - [Drain new traffic safely](https://shutdown.jscrate.dev/docs/guides/readiness-and-draining) - [Configure `newRequests`](https://shutdown.jscrate.dev/docs/configuration#shutdown) - [SC310: Readiness not withdrawn](https://shutdown.jscrate.dev/docs/codes/sc310) - [SC312: Work finished before the rejection test](https://shutdown.jscrate.dev/docs/codes/sc312) --- # SC312: Work finished too soon to test rejection > SC312 (work finished too soon to test rejection) means in-flight work ended before the new request could be sent during drain. Use a longer-running workload. Source: https://shutdown.jscrate.dev/docs/codes/sc312 Last updated: 2026-09-23 `SC312` means the active workload ended before shutdown-check could send the new request. Without older work still draining, the test cannot prove how the service handles traffic during a drain. | | | | --- | --- | | Code | `SC312` (FAIL) | | Stage | Draining traffic | | CLI exit code | 1 | | What it means | The in-flight work had already finished when the new request was due, so rejection during drain could not be observed. | | Message | `Work finished before new-request rejection could be checked; use a longer-running workload` | | First thing to check | Use a longer-running test workload, so old work is still draining when the new request is sent. | ## How do I fix it? 1. Make the controlled workload run longer. 2. Withdraw readiness immediately after `SIGTERM`. 3. Remove unnecessary delays before changing the readiness state. 4. Keep the new-request route fast enough to return before the deadline. The workload should stay active long enough for readiness polling and the new request, but still finish comfortably before `shutdown.deadlineMs`. Do not disable the rejection check merely to hide the timing problem. If your production platform can route traffic during shutdown, keep the check and make the test window reliable. ## How do I verify the fix? The corrected order is `readiness withdrawn`, then `new request rejected`, then `work request finished`. Run several times on CI to confirm the old work does not finish first under load. If readiness withdrawal itself is slow, fix that state change rather than adding an excessive artificial delay to the workload. ## Related - [Test traffic draining](https://shutdown.jscrate.dev/docs/guides/readiness-and-draining) - [Choose an in-flight workload](https://shutdown.jscrate.dev/docs/in-flight-work) - [SC311: New request accepted](https://shutdown.jscrate.dev/docs/codes/sc311) --- # SC999: Unexpected error > SC999 (unexpected error) means shutdown-check itself hit an error it did not plan for. Read the message, check the config, or report it with the timeline. Source: https://shutdown.jscrate.dev/docs/codes/sc999 Last updated: 2026-09-23 `SC999` means shutdown-check caught an error that does not belong to a normal failure stage. The message comes from the original error, so it is the best place to start. | | | | --- | --- | | Code | `SC999` (FAIL) | | Stage | Unexpected errors | | CLI exit code | 1 | | What it means | The check itself hit an error it did not expect, such as an unsafe request path or an operating-system failure. | | Message | | | First thing to check | Read the message in the result; if it points at shutdown-check rather than your service, open an issue with the timeline. | ## What should I do first? 1. Read the full message and the last timeline event. 2. If you call `runCheck()` directly, validate the object with `parseConfig()`. 3. Try the same config through `checkShutdown()` or the CLI. 4. Confirm the environment is supported and has enough system resources. A hand-built `CheckConfig` can bypass normal validation and cause errors such as an unsafe request path. The higher-level APIs catch those values before the run begins. ## When should I report a bug? Report the problem when a valid config repeatedly returns `SC999`. Include the package version, operating system, Node.js version, config with secrets removed, result message, timeline, and a small reproduction if possible. ## How do I rule out a config problem? Run the same input through `parseConfig()` or use the CLI. If validation rejects it, fix that error first. If validation passes and SC999 repeats, keep the original message unchanged for the issue report. Never include credentials, private URLs, tokens, or customer data in the reproduction. ## Related - [Node API validation](https://shutdown.jscrate.dev/docs/node-api#validate-input-with-parseconfig) - [Troubleshooting](https://shutdown.jscrate.dev/docs/troubleshooting) - [About and support](https://shutdown.jscrate.dev/docs/about) ---