Back to catalog

@bun-and-butter/shutdown

A small helper for graceful shutdowns that coordinates ordered shutdown handlers, parallel cleanup handlers, timeout enforcement, and cancellation via AbortSignal behind a simple shared API.

Tags
v1.0.0
Install

Install from Git

Bun & Butter packages are installed directly from the GitHub repository over SSH. This is the recommended install path for the current version. See the Bun docs on Git dependencies for the exact `bun add` behavior.

					bun add [email protected]:bun-and-butter/shutdown.git
				
Documentation

README.md

A small helper for graceful shutdown in Bun applications.

@bun-and-butter/shutdown gives you one shared shutdown manager for the process. It lets you register ordered shutdown handlers, parallel cleanup handlers, coordinate cancellation through an AbortSignal, and trigger a graceful application exit from one central place.

This is useful when your application needs to:

  • stop accepting new work
  • cancel long-running background tasks
  • close servers, queues, or workers in a predictable order
  • flush logs, metrics, or telemetry before the process exits
  • enforce a maximum shutdown timeout

Example

import { setTimeout as sleep } from "node:timers/promises";
import { shutdown } from "@bun-and-butter/shutdown";

shutdown.timeoutInSec = 5;

shutdown.registerShutdownHandler(async () => {
	console.log("closing HTTP server");
	await sleep(300);
});

shutdown.registerShutdownHandler(async () => {
	console.log("stopping background jobs");
	await sleep(200);
});

shutdown.registerCleanupHandler(async () => {
	console.log("flushing metrics");
	await sleep(100);
});

const runWorker = async (signal: AbortSignal) => {
	while (!signal.aborted) {
		await sleep(250);
	}
};

void runWorker(shutdown.signal);

shutdown.exit(0);

Why Use It

In many applications, shutdown logic starts out scattered across signal handlers, server files, worker loops, and logging code. That works for a while, but it tends to become inconsistent once multiple resources need to shut down in a specific order.

This package keeps that flow explicit:

  • one shared shutdown manager for the process
  • shutdown handlers run in series in registration order
  • cleanup handlers run afterwards in parallel
  • long-running tasks can observe shutdown.signal
  • graceful shutdown can be triggered from OS signals or application code

Core API

The package exposes one shared instance:

  • shutdown The default process-wide shutdown manager.

And one main method for initiating shutdown:

  • shutdown.exit(code?) Starts graceful shutdown and exits the process afterwards.

The shared instance also exposes:

  • shutdown.signal An AbortSignal that is aborted once shutdown begins.
  • shutdown.timeoutInSec The maximum number of seconds to wait for handlers.
  • shutdown.logger Optional logger used for shutdown-related messages.
  • shutdown.logExit Controls whether exits without an OS signal emit a debug log.

Handler Model

There are two handler types:

  • registerShutdownHandler(fn) Use this for critical steps that must run in order, such as closing an HTTP server before draining workers.
  • registerCleanupHandler(fn) Use this for less critical tasks that can run concurrently, such as flushing metrics or logs.

Both handler types receive:

  • the exit code
  • the OS signal, if shutdown was triggered by a signal

Logging

By default, the shared instance uses the built-in ConsoleLogger.

You can replace it with your own logger:

shutdown.logger = {
	debug(message, meta) {
		console.debug("[app:debug]", message, meta);
	},
	info(message, meta) {
		console.info("[app:info]", message, meta);
	},
	error(message, meta) {
		console.error("[app:error]", message, meta);
	},
};

You can also disable logging entirely:

shutdown.logger = undefined;

Or disable only the debug log for normal exits without a signal:

shutdown.logExit = false;

Examples

The repository currently includes one main example:

  • examples/demo.ts Shows the shared shutdown manager, custom logging, shutdown.signal, shutdown handlers, cleanup handlers, and manual shutdown.exit(0).

For most applications:

  1. configure shutdown once during startup
  2. pass shutdown.signal into long-running tasks
  3. register critical teardown steps as shutdown handlers
  4. register non-critical flushing work as cleanup handlers
  5. call shutdown.exit(code) instead of scattering exit logic across the codebase
Usage Examples

Examples

examples/demo.ts
import { setTimeout as sleep } from "node:timers/promises";
import { type Logger, shutdown } from "../src/shutdown";

// You can provide any logger implementation as long as it matches the
// package's Logger interface.
const appLogger: Logger = {
	debug(message, meta) {
		console.debug("[demo:debug]", message, meta);
	},
	info(message, meta) {
		console.info("[demo:info]", message, meta);
	},
	error(message, meta) {
		console.error("[demo:error]", message, meta);
	},
};

// Long-running tasks can listen to `shutdown.signal` and stop themselves once
// a graceful shutdown begins.
const runWorker = async (signal: AbortSignal) => {
	let tick = 0;

	while (!signal.aborted) {
		tick += 1;
		console.log(`worker tick ${tick}`);
		await sleep(250);
	}

	console.log("worker stopped because shutdown.signal was aborted");
};

// `shutdown` is the shared singleton instance exported by the package.
// Configure it once during application startup.
shutdown.timeoutInSec = 5;
shutdown.logger = appLogger;

// By default, `shutdown.logger` already uses the built-in `ConsoleLogger`.
// Assign your own logger only when you want custom formatting or routing.

// Disable shutdown logging completely:
// shutdown.logger = undefined;

// Disable only the debug log for exits without a signal:
// shutdown.logExit = false;

// Shutdown handlers run in series, in registration order.
// Use them for critical steps that should happen deterministically.
shutdown.registerShutdownHandler(async (code, signal) => {
	console.log("shutdown handler: closing HTTP server", { code, signal });
	await sleep(300);
});
shutdown.registerShutdownHandler(async () => {
	console.log("shutdown handler: draining background jobs");
	await sleep(200);
});

// Cleanup handlers run after shutdown handlers and are executed in parallel.
// Use them for non-critical work such as flushing metrics or logs.
shutdown.registerCleanupHandler(async () => {
	console.log("cleanup handler: flushing metrics");
	await sleep(150);
});
shutdown.registerCleanupHandler(async () => {
	console.log("cleanup handler: flushing logs");
	await sleep(100);
});

// Start application work and pass the shared AbortSignal into long-running tasks.
void runWorker(shutdown.signal);

console.log("demo running");
console.log("press Ctrl+C to trigger graceful shutdown via SIGINT");
console.log("or wait for the demo to call shutdown.exit(0)");

await sleep(1000);

// You can also trigger graceful shutdown manually from application code.
console.log("triggering shutdown.exit(0)");
shutdown.exit(0);

// Keep the process alive long enough for the example to hand off control
// to the shutdown manager.
await sleep(60_000);