# Lifecycle & health

> Graceful shutdown drains in-flight requests and sends `Connection: close`. Health and readiness checks keep new traffic away from an instance that is starting or stopping.

Daloy ships the lifecycle & health slice of the secure-by-default initiative: connection-draining shutdown, crash-on-unhandled-rejection, and first-class `healthcheck()` / `readinesscheck()` primitives. All three default-on the safe behaviour and let you opt out per-feature or globally with `app({ secureDefaults: false })`.

## 1. Connection-draining shutdown

**Diagram: Draining in-flight requests on shutdown**

1. **app.close(timeoutMs)** (signal) - drain flag flips synchronously
2. **New requests rejected** (new traffic) - 503 + retry-after: 5 + connection: close
3. **Existing responses finish** (in-flight) - gain connection: close header
4. **onClose hooks run** (drained) - idle keep-alive sockets killed

During the drain window new requests get a structured 503 while in-flight responses are allowed to finish, then onClose hooks run. The connection: close header stops HTTP/1.1 load balancers from reusing the socket.

`app.shutdown(timeoutMs, reason?)` (alias `app.close()`) flips a drain flag synchronously. Every subsequent request returns a structured `503 application/problem+json` with `retry-after: 5` and `connection: close`. Any in-flight response that finishes during the drain window also gains `connection: close` so HTTP/1.1 load balancers stop reusing the keep-alive socket.

```ts
const app = new App({ env: "production" });

// Trigger a graceful shutdown - drain in-flight for up to 10s, then run onClose hooks.
await app.close(10_000, "SIGTERM");

// New requests during the drain window:
//   HTTP/1.1 503 Service Unavailable
//   content-type: application/problem+json
//   retry-after: 5
//   connection: close
```

On the Node adapter, `serve(app)` registers an idle-connection close hook that calls `server.closeIdleConnections()` the moment draining begins, keep-alive sockets without an in-flight request are killed immediately, without affecting sockets that are still serving a request. Custom adapters can register the same hook via `app._registerIdleConnectionCloseHook(hook)`.

## 2. Crash on unhandled rejection in production

The new `app({ crashOnUnhandledRejection })` option installs process-wide listeners for `unhandledRejection` and `uncaughtException` that log `fatal` through the pluggable logger and call `process.exit(1)`. The framework deliberately avoids the "swallow and keep running" anti-pattern, a crashed process is easier to reason about than a zombie one. Defaults:

- Omitted: install in production (`env: "production"` or `NODE_ENV === "production"`), skip elsewhere.
- `true`: install even in development (useful for staging / CI).
- `false`: never install, even in production.

A process-wide latch ensures the listeners are installed exactly once even when multiple `App` instances boot in the same process. No-op on runtimes without `process.on` (Cloudflare Workers, Vercel, Fastly Compute).

## 3. `app.healthcheck()` and `app.readinesscheck()`

Opt-in route registration with sensible defaults: paths `/healthz` and `/readyz`, per-IP fixed-window rate limit (60 requests / 60 s, in-memory), optional bearer-token auth compared via `timingSafeEqual`. Readiness returns `503` with `retry-after: 5` while draining **or** while any plugin is still pending in `register()`.

```ts
const app = new App({ env: "production" });

// Unauthenticated probes refuse-to-boot in production:
app.healthcheck();
// Error: app.healthcheck() refused in production: provide opts.token to require
// Authorization: Bearer <token>, or pass acknowledgeUnauthenticated: true ...

// Token-required (recommended for public clusters):
app.healthcheck({ token: process.env.HEALTH_TOKEN! });
app.readinesscheck({ token: process.env.HEALTH_TOKEN! });

// Or acknowledge the surface is internal and unauthenticated is fine:
app.healthcheck({ acknowledgeUnauthenticated: true });
app.readinesscheck({
  path: "/__ready",
  acknowledgeUnauthenticated: true,
  rateLimit: { limit: 120, windowMs: 60_000 },
});
```

Pass `rateLimit: false` to disable the per-IP cap entirely (sidecar-only probes that arrive directly from the orchestrator). The limiter deliberately does **not** honour `X-Forwarded-For`: probes typically arrive from a sidecar so spoofing the header should not bypass the cap.

## Opt-out

Disable the whole slice with `new App({ secureDefaults: false })`, or just the crash handlers with `new App({ crashOnUnhandledRejection: false })`. Health and readiness routes are opt-in, no auto-registration happens, the framework only flips behaviour when you call `app.healthcheck()` / `app.readinesscheck()` explicitly.

---

Source: https://daloyjs.dev/docs/security/lifecycle-health