{"_id":"@ambicuity/shutdown-manager","name":"@ambicuity/shutdown-manager","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@ambicuity/shutdown-manager","version":"1.0.0","description":"Zero-drama shutdown orchestration for production Node.js HTTP services. Drain connections, stop new traffic, run cleanup hooks, and exit safely across Docker, Kubernetes, Express, Fastify, Koa, and native HTTP.","keywords":["graceful-shutdown","shutdown","shutdown-manager","shutdown-handler","shutdown-orchestrator","sigterm","sigint","sighup","signal","signal-handler","process-exit","exit","kubernetes","k8s","preStop","pre-stop","preStop-hook","readiness","readiness-probe","liveness","liveness-probe","healthcheck","health-check","terminationGracePeriodSeconds","rolling-deploy","zero-downtime","docker","container","tini","dumb-init","http","https","http2","http-server","https-server","keep-alive","connection-draining","drain","drain-connections","connection-tracker","express","fastify","koa","nestjs","hono","cleanup","cleanup-hooks","resource-cleanup","lifecycle","lifecycle-events","orchestration","observability","events","event-emitter","abort-signal","AbortController","timeout","production","production-ready","reliability","infrastructure","devops","sre","typescript","typescript-first","esm","cjs","dual-package","zero-dependencies","no-dependencies","node","nodejs","node-js"],"license":"MIT","author":{"name":"Ritesh Rana","email":"contact@riteshrana.engineer"},"funding":{"type":"buymeacoffee","url":"https://buymeacoffee.com/ritesh.rana"},"homepage":"https://github.com/ambicuity/shutdown-manager#readme","bugs":{"url":"https://github.com/ambicuity/shutdown-manager/issues"},"repository":{"type":"git","url":"git+https://github.com/ambicuity/shutdown-manager.git"},"type":"module","main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"sideEffects":false,"engines":{"node":">=18.17"},"scripts":{"build":"tsup","clean":"rm -rf dist coverage","typecheck":"tsc --noEmit","lint":"eslint \"{src,tests,examples}/**/*.ts\"","lint:fix":"eslint --fix \"{src,tests,examples}/**/*.ts\"","format":"prettier --write \"{src,tests,examples}/**/*.{ts,md}\" \"*.{json,md}\"","format:check":"prettier --check \"{src,tests,examples}/**/*.{ts,md}\" \"*.{json,md}\"","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","publint":"publint","attw":"attw --pack","verify":"npm run typecheck && npm run lint && npm run test && npm run build && npm run publint && npm run attw","prepublishOnly":"npm run verify"},"devDependencies":{"@arethetypeswrong/cli":"^0.18.0","@types/express":"^5.0.0","@types/koa":"^2.15.0","@types/node":"^20.14.0","@types/supertest":"^6.0.2","@typescript-eslint/eslint-plugin":"^8.0.0","@typescript-eslint/parser":"^8.0.0","@vitest/coverage-v8":"^2.1.0","eslint":"^9.10.0","eslint-config-prettier":"^9.1.0","expect-type":"^1.1.0","express":"^5.0.0","fastify":"^5.0.0","koa":"^2.15.3","prettier":"^3.3.3","publint":"^0.3.0","supertest":"^7.0.0","tsup":"^8.3.0","typescript":"^5.6.0","vitest":"^2.1.0"},"publishConfig":{"access":"public","provenance":true},"gitHead":"66998f4455631adeb396f10e456af7e0f63f2032","_id":"@ambicuity/shutdown-manager@1.0.0","_nodeVersion":"25.9.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-JeT74L2e1vLzaaGBDf3J9Msl9+pC0w46FEKk7xYn/bcobkjeusJur6VDFL5YvgChpiDc2QEG7BZB4F5Xm/7INA==","shasum":"7b4327b12dbfad9196e5fc08f9f620cbe012656d","tarball":"https://registry.npmjs.org/@ambicuity/shutdown-manager/-/shutdown-manager-1.0.0.tgz","fileCount":10,"unpackedSize":224942,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIARBXrBILrGr0aq0dueubMx5qMsEx/d50I9gw5XxGOhcAiAmOFoiyJ9sneGBvOQqa6E1fLkuEU3u4dTawkB8Ki+5XQ=="}]},"_npmUser":{"name":"ambicuity","email":"riteshrana36@gmail.com"},"directories":{},"maintainers":[{"name":"ambicuity","email":"riteshrana36@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/shutdown-manager_1.0.0_1778783951608_0.678714988324105"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-14T18:39:11.503Z","1.0.0":"2026-05-14T18:39:11.798Z","modified":"2026-05-14T18:39:12.435Z"},"maintainers":[{"name":"ambicuity","email":"riteshrana36@gmail.com"}],"description":"Zero-drama shutdown orchestration for production Node.js HTTP services. Drain connections, stop new traffic, run cleanup hooks, and exit safely across Docker, Kubernetes, Express, Fastify, Koa, and native HTTP.","homepage":"https://github.com/ambicuity/shutdown-manager#readme","keywords":["graceful-shutdown","shutdown","shutdown-manager","shutdown-handler","shutdown-orchestrator","sigterm","sigint","sighup","signal","signal-handler","process-exit","exit","kubernetes","k8s","preStop","pre-stop","preStop-hook","readiness","readiness-probe","liveness","liveness-probe","healthcheck","health-check","terminationGracePeriodSeconds","rolling-deploy","zero-downtime","docker","container","tini","dumb-init","http","https","http2","http-server","https-server","keep-alive","connection-draining","drain","drain-connections","connection-tracker","express","fastify","koa","nestjs","hono","cleanup","cleanup-hooks","resource-cleanup","lifecycle","lifecycle-events","orchestration","observability","events","event-emitter","abort-signal","AbortController","timeout","production","production-ready","reliability","infrastructure","devops","sre","typescript","typescript-first","esm","cjs","dual-package","zero-dependencies","no-dependencies","node","nodejs","node-js"],"repository":{"type":"git","url":"git+https://github.com/ambicuity/shutdown-manager.git"},"author":{"name":"Ritesh Rana","email":"contact@riteshrana.engineer"},"bugs":{"url":"https://github.com/ambicuity/shutdown-manager/issues"},"license":"MIT","readme":"<!--\n     _           _      _\n ___| |__  _   _| |_ __| | _____      ___ __        _ __ ___   __ _ _ __   __ _  __ _  ___ _ __\n/ __| '_ \\| | | | __/ _` |/ _ \\ \\ /\\ / / '_ \\ _____| '_ ` _ \\ / _` | '_ \\ / _` |/ _` |/ _ \\ '__|\n\\__ \\ | | | |_| | || (_| | (_) \\ V  V /| | | |_____| | | | | | (_| | | | | (_| | (_| |  __/ |\n|___/_| |_|\\__,_|\\__\\__,_|\\___/ \\_/\\_/ |_| |_|     |_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_|\n                                                                                 |___/\n\n  shutdown-manager  --  production shutdown orchestration for Node.js HTTP services\n\n  Author  : Ritesh Rana  <contact@riteshrana.engineer>\n  Support : https://buymeacoffee.com/ritesh.rana\n  License : MIT\n-->\n\n# `@ambicuity/shutdown-manager`\n\n> Zero-drama shutdown orchestration for production Node.js HTTP services.\n> Drain connections, stop new traffic, run cleanup hooks, and exit safely\n> across **Docker**, **Kubernetes**, **Express**, **Fastify**, **Koa**, and **native HTTP/HTTP2**.\n\n[![CI](https://github.com/ambicuity/shutdown-manager/actions/workflows/ci.yml/badge.svg)](https://github.com/ambicuity/shutdown-manager/actions/workflows/ci.yml)\n[![npm](https://img.shields.io/npm/v/@ambicuity/shutdown-manager.svg)](https://www.npmjs.com/package/@ambicuity/shutdown-manager)\n[![types](https://img.shields.io/badge/types-built--in-blue.svg)](#typescript)\n[![runtime deps](https://img.shields.io/badge/runtime--deps-0-brightgreen.svg)](#zero-runtime-dependencies)\n[![license](https://img.shields.io/npm/l/@ambicuity/shutdown-manager.svg)](LICENSE)\n[![Buy Me a Coffee](https://img.shields.io/badge/Buy_Me_a_Coffee-FFDD00?logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/ritesh.rana)\n\n**Author:** Ritesh Rana — `contact@riteshrana.engineer`\n**Support development:** [buymeacoffee.com/ritesh.rana](https://buymeacoffee.com/ritesh.rana)\n\n---\n\n## Why?\n\n`server.close()` does not actually drain your server. It stops accepting new\nconnections and waits for existing ones to close on their own — which, with\nHTTP keep-alive, **never happens until the client disconnects**. Meanwhile\nKubernetes already started routing new requests away three seconds ago,\nyour Postgres pool is sitting on uncommitted work, and the SIGKILL clock\nis ticking. That is how rolling deploys drop requests.\n\n`@ambicuity/shutdown-manager` orchestrates the full shutdown lifecycle:\n\n1. Flip readiness to _false_ so load balancers stop sending new traffic.\n2. Optionally wait for the LB to react (`preStopDelayMs`).\n3. Refuse new connections, set `Connection: close` on idle keep-alives, finish in-flight requests.\n4. Run your cleanup hooks (Postgres, Redis, queues, telemetry flush) in priority order.\n5. Exit `0` if everything was clean, `1` if a critical resource failed or a timeout fired.\n\n## Install\n\n```bash\nnpm install @ambicuity/shutdown-manager\n```\n\nRequires **Node.js ≥ 18.17**. Ships **dual ESM + CJS** with built-in TypeScript types.\n\n## Quickstart (TypeScript)\n\n```ts\nimport express from 'express';\nimport { ShutdownManager } from '@ambicuity/shutdown-manager';\n\nconst app = express();\nconst server = app.listen(3000);\n\nconst shutdown = new ShutdownManager({ timeout: 15_000 })\n  .attach(server)\n  .register('postgres', () => pool.end(), { priority: 10, critical: true })\n  .register('redis', () => redis.quit())\n  .register('bullmq', () => worker.close());\n\napp.get('/ready', (_req, res) =>\n  shutdown.isReady() ? res.send('ok') : res.status(503).send('shutting down'),\n);\n```\n\nThat's it. SIGTERM and SIGINT are wired automatically. On signal, the manager\nwalks the lifecycle and exits the process when it's done.\n\n### CommonJS\n\n```js\nconst { ShutdownManager } = require('@ambicuity/shutdown-manager');\nconst shutdown = new ShutdownManager().attach(server);\n```\n\n## Lifecycle\n\n```\n  idle  ─►  preShutdown  ─►  draining  ─►  cleanup  ─►  done | failed\n            (flip ready,     (server.close,  (run resources    (exit\n             optional         drain sockets,  by priority,      0 or 1)\n             preStop delay)   force on        critical=true\n                              timeout)        ⇒ failed)\n```\n\nEvery phase emits a typed event you can hook for logging or metrics.\n\n## API\n\n### `new ShutdownManager(options?)`\n\n```ts\ninterface ShutdownManagerOptions {\n  signals?: NodeJS.Signals[]; // default ['SIGTERM','SIGINT']\n  timeout?: number; // total budget ms, default 30_000\n  developmentMode?: boolean; // default: NODE_ENV === 'development'\n  forceExit?: boolean; // default true\n  logger?: Logger; // default: silent noop\n  poll?: { intervalMs?: number }; // default 100\n  autoStart?: boolean; // default true\n}\n```\n\n### Methods\n\n| Method                       | Purpose                                                        |\n| ---------------------------- | -------------------------------------------------------------- |\n| `.attach(server)`            | Track an `http`, `https`, `http2`, or `http2.secure` server    |\n| `.detach(server)`            | Stop tracking a server                                         |\n| `.register(name, fn, opts?)` | Register a cleanup hook                                        |\n| `.unregister(name)`          | Remove a previously registered hook                            |\n| `.kubernetes(opts)`          | Enable preStop delay + readiness flip                          |\n| `.isReady()`                 | `false` once shutdown begins (wire to your `/ready` endpoint)  |\n| `.isShuttingDown()`          | `true` while a shutdown is in progress                         |\n| `.phase()`                   | Current `LifecyclePhase`                                       |\n| `.trigger(reason?)`          | Manually run the shutdown lifecycle (returns `ShutdownResult`) |\n| `.start()` / `.stop()`       | Attach / detach signal handlers                                |\n| `.on(event, handler)`        | Typed event subscription                                       |\n\n### Resource registry\n\n```ts\nshutdown.register(\n  'postgres',\n  async (signal) => {\n    // signal is an AbortSignal — honor it for cooperative cancellation\n    await pool.end();\n  },\n  {\n    timeout: 5_000, // per-resource budget; default 10_000\n    priority: 10, // lower runs first; default 0\n    concurrency: 'parallel', // 'parallel' (default) or 'sequential' within a priority group\n    critical: true, // a failure marks the whole shutdown as failed (exit 1)\n  },\n);\n```\n\n### Events\n\n```ts\nshutdown.on('phase',            ({ name, durationMs }) => …);\nshutdown.on('connection:closed', ({ remaining, secure }) => …);\nshutdown.on('resource:start',    ({ name }) => …);\nshutdown.on('resource:done',     ({ name, durationMs }) => …);\nshutdown.on('resource:error',    ({ name, error, critical }) => …);\nshutdown.on('timeout',           ({ phase, elapsedMs }) => …);\nshutdown.on('forced',            ({ socketsDestroyed }) => …);\nshutdown.on('error',             (err) => …);\n```\n\n## Kubernetes\n\n```ts\nconst shutdown = new ShutdownManager({ timeout: 15_000 })\n  .attach(server)\n  .kubernetes({ preStopDelayMs: 5_000 });\n```\n\nWhat this does:\n\n- Flips `isReady()` to `false` _immediately_ on SIGTERM.\n- Waits `preStopDelayMs` before calling `server.close()` so the Service has\n  time to mark the pod NotReady and stop routing new traffic.\n- Lets your `terminationGracePeriodSeconds` budget remain predictable\n  (≈ `preStopDelayMs + timeout + 1–2s buffer`).\n\nSee [`examples/kubernetes/`](examples/kubernetes/) for the full Deployment YAML.\n\n## Docker\n\nSet `STOPSIGNAL SIGTERM` in your Dockerfile (Node's default `npm` wrapper\nsometimes swallows signals — running `node` directly avoids that). Use\n`tini` or `dumb-init` as PID 1 if you need to reap zombies.\n\n```dockerfile\nFROM node:20-alpine\nWORKDIR /app\nCOPY . .\nRUN npm ci --omit=dev\nSTOPSIGNAL SIGTERM\nCMD [\"node\", \"dist/server.js\"]\n```\n\n## Framework support\n\n| Framework | Attach                                                       |\n| --------- | ------------------------------------------------------------ |\n| Express   | `manager.attach(app.listen(...))`                            |\n| Koa       | `manager.attach(app.listen(...))`                            |\n| Fastify   | `manager.attach(fastify.server)`                             |\n| NestJS    | `manager.attach(app.getHttpServer())`                        |\n| Hono      | `manager.attach(serve({ fetch: app.fetch }))` (Node adapter) |\n| raw HTTP  | `manager.attach(http.createServer(...))`                     |\n| HTTP/2    | `manager.attach(http2.createSecureServer(...))`              |\n\n## Testing\n\n```ts\nimport { ShutdownManager, createTestHarness } from '@ambicuity/shutdown-manager';\n\nconst manager = new ShutdownManager({ signals: ['SIGUSR2'], forceExit: true });\nconst harness = createTestHarness({ manager });\n\nawait harness.sendSignal('SIGUSR2');\n\nexpect(harness.exitCode()).toBe(0);\nexpect(harness.timeline().map((p) => p.name)).toEqual([\n  'preShutdown',\n  'draining',\n  'cleanup',\n  'done',\n]);\n```\n\nThe harness intercepts `process.exit` so the test process stays alive.\n\n## Comparison\n\n| Feature                                 | this | http-graceful-shutdown | terminus | lil-http-terminator |\n| --------------------------------------- | :--: | :--------------------: | :------: | :-----------------: |\n| TypeScript-first                        |  ✅  |           ❌           |    ⚠️    |         ⚠️          |\n| ESM + CJS dual build                    |  ✅  |           ❌           |    ⚠️    |         ✅          |\n| Zero runtime deps                       |  ✅  |           ❌           |    ❌    |         ✅          |\n| Resource registry (priority + parallel) |  ✅  |           ❌           |    ✅    |         ❌          |\n| Typed lifecycle events                  |  ✅  |           ⚠️           |    ✅    |         ❌          |\n| Kubernetes-aware (readiness + preStop)  |  ✅  |           ❌           |    ⚠️    |         ❌          |\n| Testing harness                         |  ✅  |           ❌           |    ❌    |         ❌          |\n| npm provenance                          |  ✅  |           ❌           |    ❌    |         ❌          |\n\n## How to start using this in an existing codebase?\n\n```ts\nimport { ShutdownManager } from '@ambicuity/shutdown-manager';\nconst manager = new ShutdownManager({ timeout: 10_000 }).attach(server);\n```\n\n## Zero runtime dependencies\n\nThis package has **zero** runtime dependencies. It uses only the Node.js standard library. Releases are published with `--provenance` and signed via GitHub Actions OIDC.\n\n## Support\n\nIf this package saves you a 3am incident, consider buying me a coffee:\n[buymeacoffee.com/ritesh.rana](https://buymeacoffee.com/ritesh.rana).\n\nFor commercial support or custom integration questions, email\n`contact@riteshrana.engineer`.\n\n## License\n\nMIT © Ritesh Rana (`contact@riteshrana.engineer`)\n","readmeFilename":"README.md","_rev":"1-0b86f7684b41571c55c6da4bba84d7d8"}