Graceful Shutdown
Problem
Section titled “Problem”You need to manage the full production lifecycle of an HTTP server — connecting to databases before serving requests, draining in-flight requests on shutdown signals, and cleaning up resources (database pools, OpenTelemetry SDK, background jobs) before the process exits.
Solution
Section titled “Solution”ergo-router’s graceful() utility manages the complete server
lifecycle: startup prerequisites, signal handling, connection draining,
and orderly shutdown.
Database Connection Lifecycle
Section titled “Database Connection Lifecycle”import { handler } from "@centralping/ergo";import { graceful } from "@centralping/ergo-router";
const pipeline = handler(/* ... your pipeline ... */);
const { server, shutdown } = await graceful(pipeline, { port: 3000, onStartup: async ({ log }) => { await db.connect(); log.info("Database connected"); }, onShutdown: async ({ log, signal }) => { await db.disconnect(); log.info(`Database disconnected (${signal})`); },});import createRouter, { graceful } from "@centralping/ergo-router";
const router = createRouter({ /* ...options... */ });router.get("/users", { /* ...routes... */ });
const { server, shutdown } = await graceful(router.handle(), { port: 3000, onStartup: async ({ log }) => { await db.connect(); log.info("Database connected"); }, onShutdown: async ({ log, signal }) => { await db.disconnect(); log.info(`Database disconnected (${signal})`); },});OpenTelemetry SDK Lifecycle
Section titled “OpenTelemetry SDK Lifecycle”import { NodeSDK } from "@opentelemetry/sdk-node";import { graceful } from "@centralping/ergo-router";
const sdk = new NodeSDK({ /* ...config... */ });
const { server, shutdown } = await graceful(pipeline, { port: 3000, onStartup: async ({ log }) => { sdk.start(); log.info("OpenTelemetry SDK started"); }, onShutdown: async ({ log }) => { await sdk.shutdown(); log.info("OpenTelemetry SDK flushed and shut down"); },});import { NodeSDK } from "@opentelemetry/sdk-node";import createRouter, { graceful } from "@centralping/ergo-router";
const sdk = new NodeSDK({ /* ...config... */ });const router = createRouter({ /* ...options... */ });
const { server, shutdown } = await graceful(router.handle(), { port: 3000, onStartup: async ({ log }) => { sdk.start(); log.info("OpenTelemetry SDK started"); }, onShutdown: async ({ log }) => { await sdk.shutdown(); log.info("OpenTelemetry SDK flushed and shut down"); },});Health Check with Shutdown State
Section titled “Health Check with Shutdown State”import { graceful } from "@centralping/ergo-router";
let isShuttingDown = false;
const { server, shutdown } = await graceful(pipeline, { port: 3000, onShutdown: async ({ log }) => { isShuttingDown = true; await db.disconnect(); },});
// Expose shutdown state in your health check route// check the closure variable `isShuttingDown` in your handlerfunction healthCheck() { if (isShuttingDown) { return { response: { statusCode: 503, body: { status: "draining" } } }; } return { response: { body: { status: "healthy" } } };}import createRouter, { graceful } from "@centralping/ergo-router";
let isShuttingDown = false;
const router = createRouter();
router.get("/health", { authorization: false, execute: (req, res, acc) => { if (isShuttingDown) { return { response: { statusCode: 503, body: { status: "draining" } } }; } return { response: { body: { status: "healthy" } } }; },});
const { server, shutdown } = await graceful(router.handle(), { port: 3000, onShutdown: async ({ log }) => { isShuttingDown = true; await db.disconnect(); },});Explanation
Section titled “Explanation”Lifecycle Flow
Section titled “Lifecycle Flow”onStartup({log}) │ ▼server.listen(port, hostname) │ ▼◀── serving requests ──▶ │ │ (SIGINT or SIGTERM received) ▼server.close() ← stops accepting new connections │ ▼onShutdown({log, signal}) │ ▼ waiting for in-flight connections to drain... │ ├── connections drained → "Shutdown complete" → exit(0) │ └── timeout exceeded → server.closeAllConnections() → exit(1)Configuration Options
Section titled “Configuration Options”| Option | Default | Description |
|---|---|---|
port | 3000 | Port to listen on |
hostname | '0.0.0.0' | Hostname to bind to |
log | console | Logger with .info(), .warn(), .error() methods |
signals | ['SIGINT', 'SIGTERM'] | OS signals that trigger shutdown |
timeout | 5000 | Maximum time (ms) to wait for connections to drain |
exit | process.exit | Exit function (override for testing) |
onStartup | — | Hook called before server.listen() |
onShutdown | — | Hook called after server.close() |
Startup Failure
Section titled “Startup Failure”If onStartup rejects (throws), the server never starts listening. Use
this to enforce prerequisites — database connectivity, config
validation, secret loading — before accepting traffic.
Timeout Backstop
Section titled “Timeout Backstop”The timeout option provides a hard deadline for graceful shutdown. If
in-flight connections do not close within the timeout window, the server
forcefully terminates all connections and exits with code 1. Set the
timeout to match your load balancer’s drain timeout.
Programmatic Shutdown
Section titled “Programmatic Shutdown”graceful() returns { server, shutdown }. Call shutdown(signal)
programmatically to initiate the same graceful shutdown flow without
waiting for an OS signal. This is useful for health-check-driven
shutdown or integration tests.
For test-specific lifecycle patterns (ephemeral ports, preventing
process.exit), see Testing Patterns.
Custom Logger Integration
Section titled “Custom Logger Integration”The log option accepts any object with .info(), .warn(), and
.error() methods — a pino or winston instance works directly. For
complete integration patterns including request ID correlation and trace
enrichment, see Structured Logging.
Multiple Resource Cleanup
Section titled “Multiple Resource Cleanup”Chain cleanup operations in onShutdown. Errors in onShutdown are
caught and logged — they do not prevent the shutdown from completing:
onShutdown: async ({ log, signal }) => { await db.disconnect(); await cache.quit(); await sdk.shutdown(); log.info(`All resources released (${signal})`);}