A low-level lifecycle manager for Node.js. Automatically tracks network buffers and guarantees that data sent in fire-and-forget mode (to Redis, RabbitMQ, Postgres, etc.) is not lost when the application exits or on critical errors.
You no longer need to call redis.quit() or channel.close() manually. The package watches system TCP sockets and prevents Node.js from exiting until the internal socket buffer (writableLength) becomes 0.
It works with any type of server natively, and with servers like WebSocketServer register those with trackServer because under the hood they are EventEmitter instances, not native HTTP servers:
import { trackServer } from 'universal-graceful-exit';
const wss = new WebSocketServer({ port: config.port });
trackServer(wss);Register outbound clients (ioredis, amqplib, pg, ...) with trackClient so they get quit / close / end / disconnect on shutdown:
import { trackClient } from 'universal-graceful-exit';
trackClient(redis);
trackClient(amqpConnection);Call untrackClient when you close or replace a tracked client. The library keeps a reference to every tracked client until shutdown; if you reconnect for weeks without untracking, those already-closed objects stay in memory. A second quit / close on shutdown is usually harmless; the leak is not.
import { trackClient, untrackClient } from 'universal-graceful-exit';
async function reconnectRedis() {
if (redis) {
await redis.quit();
untrackClient(redis);
}
redis = new Redis(config.redisUrl);
trackClient(redis);
}The same pattern applies to a pool or request-scoped client you drain yourself (pool.end(), channel.close(), ...): close it, then untrackClient so this library drops its reference.
npm install universal-graceful-exitImport the package as the very first line of your application:
import 'universal-graceful-exit'; // It intercepts signals and unhandledRejections on its own
import { isStopped } from 'universal-graceful-exit';
import { myRabbitClient } from './db';
async function worker() {
while (!isStopped()) {
// Your business logic
myRabbitClient.sendToQueue('tasks', Buffer.from('data')); // no await!
}
}import { isStopped, startTask, endTask } from 'universal-graceful-exit';
async function startConsuming(rabbitConn) {
const rabbitChannel = await rabbitConn.createChannel();
await rabbitChannel.prefetch(200);
const { consumerTag } = await rabbitChannel.consume(
'ordered_events',
async msg => {
if (!msg) return;
startTask();
const result = await processEvent(msg.content);
if (msg.properties.replyTo) {
rabbitChannel.sendToQueue(msg.properties.replyTo, Buffer.from(result), {
correlationId: msg.properties.correlationId
});
}
rabbitChannel.ack(msg);
endTask();
if (isStopped()) {
await rabbitChannel.cancel(consumerTag).catch(() => {});
}
},
{ noAck: false }
);
}If you need to trigger a graceful shutdown from inside your application logic (e.g., an admin route or a critical business workflow), you can emit a custom event on the global process object. This allows you to pass a custom exit code without forcing a hard crash:
import 'universal-graceful-exit';
// Trigger a safe, buffered exit with a custom exit code (e.g., 5)
process.emit('gracefulExit', 5);Log example of usage with ws and ioredis libraries
[UniversalExit] Received OS signal: SIGINT
[UniversalExit] Clean shutdown initiated. Custom Servers: 1, Native Servers: 1, Sockets: 4
[UniversalExit] Custom server clients: 1
[UniversalExit] currentTasks: 0, currentBytes: 0, customServers: 1, clients: 4
[UniversalExit] Closing client via quit...
[UniversalExit] Closing client via quit...
[UniversalExit] Closing client via quit...
[UniversalExit] Closing client via quit...
[UniversalExit] Scheduling next check #0. Sockets left: 4
[UniversalExit] Native server close event
[UniversalExit] Custom server close event
[WARN] EventServer - Websocket Server closed
[DEBUG] EventServer - Websocket Client Connection closed
[UniversalExit] Socket natively CLOSED. Left in set: 3
[UniversalExit] Socket natively CLOSED. Left in set: 2
[UniversalExit] Socket natively CLOSED. Left in set: 1
[UniversalExit] Socket natively CLOSED. Left in set: 0
[UniversalExit] currentTasks: 0, currentBytes: 0, customServers: 0, clients: 0
[UniversalExit] All buffers, tasks, servers and clients cleared successfully. Exiting cleanly.
[DEBUG] App - Shut down complete!