npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

graceful-shutdown-sri

v1.0.0

Published

Production-ready, framework-agnostic graceful shutdown manager for Node.js: drains HTTP servers and safely releases databases, queues, caches and custom resources on SIGTERM/SIGINT and fatal errors.

Readme

graceful-shutdown-sri

CI npm license

A production-ready, framework-agnostic graceful shutdown manager for Node.js.

When your process receives SIGTERM/SIGINT or crashes with an uncaughtException or unhandledRejection, graceful-shutdown-sri stops accepting traffic, lets in-flight requests finish, and closes your WebSockets, queues, caches and databases in the right order within a time budget. Then it exits with a meaningful exit code.

  • Zero runtime dependencies. Adapters use structural types, so they don't import the libraries they close.
  • Fully typed TypeScript API, with ESM and CommonJS builds. Tree-shakeable (sideEffects: false).
  • Node.js 18+.
  • Correct HTTP draining: idle keep-alive sockets are closed, in-flight requests finish and get Connection: close, and leftovers are force-destroyed on timeout.
  • Ordering: built-in priority tiers plus dependsOn, run sequential or parallel.
  • Timeouts per resource and globally. Resources get an AbortSignal so they can switch to a forced close.
  • Observability: typed lifecycle events and a detailed shutdown report.
  • Leak-safe: every listener it attaches can be removed (unlisten(), dispose(), unregister functions), and its timers never keep the process alive.

Built-in adapters: HTTP, HTTPS, Express, Fastify, Socket.IO, ws, Redis (ioredis / node-redis), Sequelize, TypeORM, Prisma, BullMQ, RabbitMQ (amqplib), KafkaJS, MongoDB / Mongoose, PostgreSQL (pg / postgres.js), MySQL (mysql2 / mysql), plus any custom task.

Installation

npm install graceful-shutdown-sri

Quick start

import http from 'node:http';
import { createShutdownManager } from 'graceful-shutdown-sri';

const server = http.createServer(app);

const shutdown = createShutdownManager({
  timeout: 15_000,
  logger: console,
});

shutdown.registerHttpServer(server);

shutdown.registerTask(async () => {
  await redis.disconnect();
});

shutdown.registerTask(async () => {
  await sequelize.close();
});

shutdown.registerTask(async () => {
  await queue.close();
});

shutdown.listen();
server.listen(3000);

Or use the adapters, which know each library's safest close sequence and put it in the right priority tier:

import {
  createShutdownManager,
  redisResource,
  sequelizeResource,
  bullMqResource,
} from 'graceful-shutdown-sri';

const shutdown = createShutdownManager({ timeout: 15_000 });

shutdown.registerHttpServer(server); // stopped first
shutdown.register(bullMqResource([worker, queue])); // then queue workers
shutdown.register(redisResource(redis)); // databases and caches last
shutdown.register(sequelizeResource(sequelize));

shutdown.listen();

On SIGTERM this logs something like:

[graceful-shutdown-sri] Received SIGTERM
[graceful-shutdown-sri] Shutdown started (signal SIGTERM), 4 resource(s), timeout 15000ms
[graceful-shutdown-sri] Closed "http-server" in 38ms
[graceful-shutdown-sri] Closed "bullmq" in 412ms
[graceful-shutdown-sri] Closed "redis" in 3ms
[graceful-shutdown-sri] Closed "sequelize" in 5ms
[graceful-shutdown-sri] Shutdown complete in 460ms

Then the process exits with code 0.

Frameworks

Express

app.listen() returns an http.Server, so register that:

const app = express();
const server = app.listen(3000);
shutdown.registerHttpServer(server);

Fastify

import { fastifyResource } from 'graceful-shutdown-sri';

const app = Fastify();
shutdown.register(fastifyResource(app));
await app.listen({ port: 3000 });

HTTPS

import https from 'node:https';

const server = https.createServer({ key, cert }, app);
shutdown.registerHttpServer(server); // TLS is detected automatically

Socket.IO and ws

Realtime servers are in the highest tier, so clients are told to disconnect before the HTTP server stops:

import { socketIoResource, webSocketServerResource } from 'graceful-shutdown-sri';

shutdown.register(socketIoResource(io));
shutdown.register(webSocketServerResource(wss)); // sends close code 1001, terminates on timeout
shutdown.registerHttpServer(server);

Resource adapters

| Adapter | Accepts | Close behaviour | Default tier | | ------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------- | ------------ | | httpServerResource / registerHttpServer | http.Server, https.Server, Express app.listen() | Stop accepting, close idle sockets, drain in-flight, force on timeout | SERVER | | httpsServerResource | https.Server | Same, TLS-aware | SERVER | | fastifyResource | Fastify instance | app.close() | SERVER | | socketIoResource | Socket.IO Server | disconnectSockets(true) + close() | REALTIME | | webSocketServerResource | ws WebSocketServer | Close frame to clients, wait, terminate() on timeout | REALTIME | | bullMqResource | Worker, Queue, QueueEvents, FlowProducer (one or an array) | close() in order, close(true) on timeout | MESSAGING | | rabbitMqResource | amqplib connection, or { connection, channels } | Close channels, then the connection | MESSAGING | | kafkaResource | KafkaJS consumer / producer / admin (one or an array) | disconnect() in order | MESSAGING | | redisResource | ioredis, node-redis v4/v5 | quit() (or close()), disconnect() on timeout | DATABASE | | sequelizeResource | Sequelize | close() | DATABASE | | typeOrmResource | DataSource (0.3) / Connection (0.2) | destroy() / close(), skipped if not initialized | DATABASE | | prismaResource | PrismaClient | $disconnect() | DATABASE | | mongoDbResource | MongoClient, Mongoose connection / mongoose | close(), close(true) on timeout (or disconnect()) | DATABASE | | postgresResource | pg Pool / Client, postgres.js sql | end() | DATABASE | | mySqlResource | mysql2 pool/connection (callback or promise API), mysql | end() | DATABASE |

Every adapter accepts { name, priority, dependsOn, timeout } as its last argument. See docs/resources.md for notes on each one.

Shutdown order

By default, resources are stopped in priority tiers, highest first:

| Tier | Value | Meant for | | -------------------- | ----- | --------------------------------------------------------------------------- | | Priority.REALTIME | 400 | Socket.IO, ws: tell clients to go away before the server stops | | Priority.SERVER | 300 | HTTP/HTTPS/Fastify: stop accepting traffic and drain requests | | Priority.MESSAGING | 200 | Queue workers, consumers, producers | | Priority.DEFAULT | 100 | Custom registerTask tasks | | Priority.DATABASE | 0 | Databases and caches: everything above may still need them, so they go last |

Within a tier, resources run in registration order.

Dependencies override priorities. dependsOn lists resources that this resource uses. A resource is always stopped before the resources it depends on:

shutdown.register(redisResource(redis, { name: 'cache' }));
shutdown.register(prismaResource(prisma, { name: 'db' }));

// The outbox flusher writes to the DB and reads the cache, so it must run before both close.
shutdown.registerTask(flushOutbox, { name: 'outbox', priority: 0, dependsOn: ['db', 'cache'] });

Cycles are rejected with a DependencyCycleError at registration time. Unknown names produce a warning at shutdown and are ignored.

Execution mode. sequential (the default) stops one resource at a time. parallel stops all resources of the same phase concurrently. A phase is a set of resources with the same priority whose dependents have already stopped, so ordering guarantees hold in both modes. shutdown.getExecutionPlan() returns the phases, which is useful for checking your setup:

console.log(shutdown.getExecutionPlan());
// [ ['socket.io'], ['http-server'], ['bullmq'], ['outbox'], ['cache', 'db'] ]

Options

interface ShutdownOptions {
  timeout?: number; // 30000. Global time budget in ms (0 = none)
  logger?: Logger | false; // console. Anything with info/warn/error(/debug). false = silent
  exitProcess?: boolean; // true. Call process.exit() when done
  closeIdleConnections?: boolean; // true. HTTP: drop idle keep-alive sockets immediately
  closeAllConnections?: boolean; // false. HTTP: drop every socket immediately
  forceExitOnTimeout?: boolean; // true. On global timeout, exit(1) right away
  executionMode?: 'sequential' | 'parallel'; // 'sequential'
  debug?: boolean; // false. Verbose logs
  signals?: NodeJS.Signals[]; // ['SIGTERM', 'SIGINT']
  handleUncaughtException?: boolean; // true
  handleUnhandledRejection?: boolean; // true
  forceExitOnSecondSignal?: boolean; // true. A second Ctrl+C exits immediately
  preShutdownDelay?: number; // 0. Wait before closing (Kubernetes endpoint propagation)
}

Exit codes: 0 when every resource closed successfully. 1 when any resource failed or timed out, when the global timeout elapsed, or when the shutdown was caused by uncaughtException/unhandledRejection.

With forceExitOnTimeout: false, a global timeout sets process.exitCode = 1 and removes the signal handlers instead of calling process.exit(), so the process ends once the event loop drains.

API

const shutdown = createShutdownManager(options);

shutdown.register(resource); // → unregister()
shutdown.registerTask(fn, { name, priority, dependsOn, timeout }); // → unregister()
shutdown.registerHttpServer(server, { closeIdleConnections, closeAllConnections, ...}); // → unregister()

shutdown.listen(); // attach signal/error handlers (idempotent)
shutdown.unlisten(); // detach them
await shutdown.shutdown('reason'); // trigger manually → ShutdownReport (idempotent)

shutdown.isShuttingDown(); // for readiness probes
shutdown.state; // 'idle' | 'shutting-down' | 'terminated'
shutdown.getReport(); // last ShutdownReport
shutdown.getExecutionPlan(); // string[][]

shutdown.on(event, listener); // → off()
shutdown.once(event, listener);
shutdown.off(event, listener);
shutdown.dispose(); // unlisten + release every resource and listener

Full reference: docs/api.md.

Custom resources

Anything with a close() works. The context gives you the reason, an AbortSignal that fires when your time is up, and the logger:

shutdown.register({
  name: 'metrics-flusher',
  priority: Priority.DEFAULT,
  dependsOn: ['db'],
  timeout: 5_000,
  async close({ signal, reason, logger }) {
    logger.info(`flushing metrics (${reason.type})`);
    await metrics.flush({ signal }); // stop early if we run out of time
  },
});

To write a reusable adapter, return a ShutdownResource. If your adapter attaches listeners at registration time, implement dispose() too, so unregistering releases them.

Events and reports

shutdown.on('shutdown:start', ({ reason }) =>
  metrics.increment('shutdown', { reason: reason.type }),
);
shutdown.on('resource:error', ({ name, error }) =>
  sentry.captureException(error, { tags: { name } }),
);
shutdown.on('resource:timeout', ({ name }) => logger.warn(`${name} was too slow`));
shutdown.on('shutdown:complete', (report) => logger.info(report, 'shutdown report'));

| Event | Payload | | ------------------- | ---------------------------------------- | | shutdown:start | { reason } | | resource:start | { name } | | resource:success | ResourceResult | | resource:error | ResourceResult (with error) | | resource:timeout | ResourceResult | | shutdown:timeout | { timeoutMs } | | shutdown:complete | ShutdownReport, emitted before exiting |

A ShutdownReport contains reason, startedAt, finishedAt, durationMs, success, timedOut, exitCode and one { name, status, durationMs, error? } entry per resource. status is one of success, failed, timeout or skipped. A throwing listener is logged and never breaks the shutdown.

Kubernetes

const shutdown = createShutdownManager({
  timeout: 25_000, // below terminationGracePeriodSeconds (default 30s)
  preShutdownDelay: 5_000, // let endpoints/load balancers stop routing to this pod
});

app.get('/readyz', (_req, res) => {
  res.status(shutdown.isShuttingDown() ? 503 : 200).end();
});

See docs/guides.md for details, and for Docker CMD advice (PID 1 must receive the signal).

Error handling

  • A resource that throws or rejects is recorded as failed. The rest still run.
  • A resource that exceeds its timeout is recorded as timeout and its AbortSignal fires. The rest still run.
  • When the global timeout elapses, every running resource is aborted, the pending ones are skipped, and the process exits with 1.
  • An uncaughtException or unhandledRejection is logged and triggers a shutdown with exit code 1. Errors raised during shutdown are logged, not re-thrown.
  • A second signal during shutdown forces an immediate exit (forceExitOnSecondSignal).
  • A logger or event listener that throws can never interrupt the shutdown.

With exitProcess: false, the handlers still catch uncaughtException, so the process keeps running after a crash. Only disable exitProcess if something else is responsible for exiting.

Testing your app

const shutdown = createShutdownManager({ exitProcess: false, logger: false });
// ...register resources...
const report = await shutdown.shutdown('test');
expect(report.success).toBe(true);
shutdown.dispose();

Examples

See examples/: Express, Fastify, a full stack (HTTP + Socket.IO + BullMQ + Redis + Prisma), Kubernetes, and custom resources.

Contributing

npm ci
npm test            # vitest
npm run test:coverage
npm run lint && npm run typecheck && npm run format:check
npm run build && npm run smoke

Commits follow Conventional Commits. Releases are published by semantic-release from main.

License

MIT