Search docs

Jump between documentation pages.

Lifecycle & health

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

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 Edge, 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.