@ololoepepe/termination-handler
@ololoepepe/termination-handler
Graceful process termination for Node.js services.
Listens for termination signals (SIGTERM / SIGINT by default), runs the
shutdown handlers you registered — in priority order — and then exits. If the
handlers take too long, the process is killed anyway, so a stuck connection can
never leave a container hanging until the orchestrator SIGKILLs it.
Installation
npm install @ololoepepe/termination-handler
Requires Node.js >= 24.
To terminate HTTP servers gracefully (draining keep-alive connections) also install the optional peer dependency:
npm install http-terminator
Without it everything else still works — see HTTP servers below.
Usage
import {TerminationHandler} from '@ololoepepe/termination-handler';
const terminationHandler = new TerminationHandler();
terminationHandler.attachHttpServer(server);
terminationHandler.attachKnexInstance(knex);
terminationHandler.attachRedisInstance(redis);
terminationHandler.installHandler(async () => {
await flushMetrics();
});
That is the whole setup: from this point on, a SIGTERM drains the HTTP server
first, then closes the database and Redis connections along with your own
handler, then exits with code 0.
How termination works
- A termination signal arrives.
- A "forced termination" timer starts. If it fires before the handlers are
done, the process exits with code
1— no matter what is still pending. - Handlers run grouped by priority, in ascending order. All handlers within one priority group run concurrently; the next group only starts once the previous one has fully settled.
- Once every group has settled, the timer is cleared and the process exits with
code
0.
Handler failures are never fatal: every group is awaited with
Promise.allSettled, and the built-in attach* helpers swallow errors from the
underlying client. A connection that refuses to close cleanly will not prevent
the remaining handlers from running.
Priorities exist so that things get shut down in a sensible order. Servers that
accept inbound traffic (attachHttpServer, attachWsServer) default to
priority 0, so they stop accepting work before the resources they depend
on — databases, queues, caches — are torn down at the default priority 1.
API
new TerminationHandler(options?)
| Option | Type | Default | Description |
|---|---|---|---|
forcedTerminationTimeout |
number |
14000 |
Milliseconds before the process is forcibly exited with code 1. |
gracefulTerminationTimeout |
number |
10000 |
Milliseconds http-terminator waits for in-flight requests before destroying sockets. |
terminationSignals |
string[] |
['SIGTERM', 'SIGINT'] |
Signals to listen for. |
Keep forcedTerminationTimeout below your orchestrator's grace period (for
example Kubernetes' terminationGracePeriodSeconds, 30s by default) so that the
process gets to exit on its own terms rather than being SIGKILLed. The default
pair leaves a 4s margin between the graceful and the forced timeout.
installHandler(handler, priority = 1)
Registers a shutdown handler. handler is called with no arguments and may
return a promise. Lower priority runs earlier.
terminationHandler.installHandler(async () => {
await consumer.stop();
}, 0);
attach* helpers
Convenience wrappers around installHandler for common clients. Each one is a
one-liner around the library's own shutdown method, with errors suppressed.
| Method | Calls | Priority |
|---|---|---|
attachHttpServer(server) |
http-terminator |
0 |
attachWsServer(server) |
server.close() |
0 |
attachAmqpInstance(instance) |
instance.close() |
1 |
attachFirebaseAdminAppInstance(instance) |
instance.delete() |
1 |
attachFirestoreInstance(instance) |
instance.terminate() |
1 |
attachKnexInstance(instance) |
instance.destroy() |
1 |
attachRedisInstance(instance) |
instance.quit() |
1 |
The helpers are duck-typed — anything exposing the right method works, so they are not tied to one particular client library.
HTTP servers
attachHttpServer uses http-terminator
to shut a server down gracefully: it stops accepting new connections, lets
in-flight requests finish, and only then destroys the lingering keep-alive
sockets. http-terminator is an optional peer dependency, imported lazily
inside the handler, so it is only needed if you actually call this method.
If it is not installed, the handler logs a warning and falls back to
server.close(). That still stops new connections from being accepted, but it
does not drain keep-alive sockets — close() waits for existing connections
to end on their own, which for keep-alive clients may not happen before
forcedTerminationTimeout kicks in. Install http-terminator if you serve
keep-alive traffic.
License
UNLICENSED — private package.