quiesce
v0.1.4
Published
Ordered, timeboxed graceful shutdown for Node — register named async hooks, drain them LIFO on SIGTERM with per-hook timeouts and a hard deadline.
Maintainers
Readme
quiesce
Zero-runtime-dependency graceful shutdown for Node.js — register named async hooks, drain them LIFO on SIGTERM with per-hook timeouts and a hard deadline.
The problem
Node.js applications need to shut down gracefully when receiving SIGTERM. Without proper shutdown handling, connections get cut mid-request, in-memory state is lost, and downstream services see cascading failures. The built-in process.on('SIGTERM') works for single cleanup tasks, but coordinating multiple async resources with different timeout requirements requires significant boilerplate.
Managing the shutdown sequence manually introduces common bugs: hooks running in wrong order, slow hooks blocking fast ones, timeout handling scattered across code, and double-signal detection for forced exits. These issues surface in production when deployments time out or processes hang during termination.
Install
npm install quiesce
# or
pnpm add quiesce
# or
yarn add quiesceUse
import { quiesce } from "quiesce";
quiesce.add("database", async () => await db.close(), { timeout: 5000 });
quiesce.arm();import { createQuiesce } from "quiesce";
import { createServer } from "http";
const shutdown = createQuiesce();
const server = createServer((req, res) => {
if (shutdown.isDraining) {
res.writeHead(503);
return res.end("Server shutting down");
}
res.writeHead(200);
res.end("OK");
});
server.listen(3000);
shutdown.add("http-server", () => new Promise((resolve) => server.close(() => resolve())));
shutdown.add("redis", () => redis.quit(), { timeout: 2000 });
shutdown.add("database", () => db.close(), { timeout: 5000 });
shutdown.arm({ deadline: 10000, onEvent: (e) => {
if (e.type === "drain-start") console.log("Shutting down...");
}});API
createQuiesce(): Quiesce
Creates a new isolated quiesce instance.
quiesce: Quiesce
Default shared quiesce instance.
interface Quiesce
add(name: string, fn: () => unknown, opts?: HookOptions): () => void — Register a named hook. Returns unregister function. LIFO execution. Throws on duplicate name or during drain.
arm(opts?: ArmOptions): void — Install signal handlers. Idempotent. Defaults: signals ["SIGTERM","SIGINT"], deadline 30000ms. Second signal forces exit code 130.
now(reason?: string): Promise<boolean> — Trigger drain programmatically. Returns true if all hooks succeeded. Concurrent-safe.
readonly isDraining: boolean — Whether a drain is in progress.
readonly size: number — Number of registered hooks.
interface HookOptions
timeout?: number — Max wait time for this hook (default: 10000ms). Exceeded hooks become stragglers.
interface ArmOptions
signals?: readonly NodeJS.Signals[] — Signals to listen for (default: ["SIGTERM", "SIGINT"]).
deadline?: number — Total deadline for drain (default: 30000ms). Remaining hooks skipped when reached.
onEvent?: (e: DrainEvent) => void — Observe drain events for logging/monitoring.
exit?: (code: number) => never — Process exit function (default: process.exit). Test seam.
type DrainEvent
{ type: "drain-start"; reason: string } | { type: "hook-start" | "hook-ok"; name: string } | { type: "hook-error"; name: string; error: Error } | { type: "hook-timeout"; name: string; timeout: number } | { type: "drain-end"; ok: boolean; stragglers: string[] }
Non-goals
quiesce will never add built-in support for:
- HTTP/HTTPS server awareness — compose with
server.close()in your hook - Health-check endpoints — use your framework's health checks with
quiesce.isDraining - Cluster/worker coordination — manage worker signaling separately
- Configuration files — configure via API:
quiesce.arm({ deadline: 30000 }) - Startup lifecycle — quiesce only handles shutdown
import { quiesce } from "quiesce";
app.get("/health", (req, res) => {
if (quiesce.isDraining) return res.status(503).json({ status: "shutting-down" });
res.json({ status: "healthy" });
});
quiesce.add("http-server", () => server.close());
quiesce.arm();Related Packages
Caching & Concurrency:
- @azghr/filterkit — Framework-agnostic, type-safe filtering for TypeScript
- @azghr/singlet — Deduplicate concurrent async calls
- staleness — Stale-while-revalidate caching for async functions
Text Processing:
- @azghr/shorn — Truncate strings by byte budget without breaking graphemes
- seriatim — Sequential processing utilities
HTTP & Network:
- forbear — Read server rate-limit instructions from HTTP responses
- forestall — Delay execution until a condition is met
- obviate — Render operations unnecessary through caching
System & Process:
- sortition — Deterministic percentage rollouts and A/B bucketing
- stanch — Stop flows or operations based on conditions
Utilities:
- expunge — Remove or exclude items from collections
- occlude — Hide or mask data and functionality
- placemark — Geographic location and mapping utilities
- specie — Currency and financial calculations
License
MIT
