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