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