@joint-ops/kaput
v0.1.0
Published
Zero-config graceful shutdown for Node.js
Downloads
92
Readme
kaput
Zero-config graceful shutdown for Node.js
The Problem
Graceful shutdown in Node.js is harder than it looks. Here's what most apps do:
// 50+ lines of boilerplate that's still broken
process.on('SIGTERM', async () => {
console.log('Shutting down...');
// Hope this order is right...
server.close();
await redis.quit();
await prisma.$disconnect();
// Did we forget something?
// What about in-flight requests?
// What if close() hangs?
process.exit(0);
});
// Oh wait, SIGINT too
process.on('SIGINT', /* copy-paste the same thing */);
// And uncaught exceptions...
// And unhandled rejections...
// And health checks during shutdown...The Solution
import 'kaput';That's it. kaput handles everything automatically.
Features
- Zero config — Auto-detects servers, databases, and caches
- Correct shutdown order — HTTP first, databases last
- Health checks — Returns 503 during shutdown
- Kubernetes ready — Works with SIGTERM, liveness, and readiness probes
- Handles edge cases — Timeouts, stuck connections, force exit
- TypeScript first — Full type definitions included
Installation
npm install @joint-ops/kaputQuick Start
Magic Import (Recommended)
Add one line at the top of your entry file:
import 'kaput';
import express from 'express';
const app = express();
app.get('/', (req, res) => res.send('Hello'));
app.listen(3000);
// kaput automatically:
// - Detects the HTTP server
// - Handles SIGTERM/SIGINT
// - Waits for in-flight requests
// - Exits cleanlyExplicit Registration
When you need more control:
import { kaput } from '@joint-ops/kaput';
import express from 'express';
import { PrismaClient } from '@prisma/client';
import Redis from 'ioredis';
const app = express();
const prisma = new PrismaClient();
const redis = new Redis();
const server = app.listen(3000);
// Register resources explicitly
kaput.register(server);
kaput.register(prisma);
kaput.register(redis);Health Checks
Return 503 during shutdown so load balancers stop sending traffic:
import { kaput } from '@joint-ops/kaput';
app.get('/health', (req, res) => {
if (kaput.isShutdown()) {
return res.status(503).json({ status: 'shutting_down' });
}
res.json({ status: 'healthy' });
});Supported Resources
| Resource | Auto-detected | Shutdown Priority | |----------|--------------|-------------------| | HTTP/HTTPS Server | Yes | 10 (first) | | Express/Fastify | Yes | 10 | | Prisma Client | Yes | 60 (last) | | Redis (ioredis) | Yes | 50 | | BullMQ Worker | Yes | 30 | | BullMQ Queue | Yes | 40 | | Mongoose | Yes | 60 | | Knex | Yes | 60 | | Custom (close/disconnect) | Manual | 100 |
Configuration
import { Kaput } from '@joint-ops/kaput';
const kaput = new Kaput({
timeout: 30000, // Max shutdown time (default: 30s)
gracePeriod: 10000, // Wait for in-flight requests (default: 10s)
signals: ['SIGTERM', 'SIGINT'],
logLevel: 'info', // 'debug' | 'info' | 'warn' | 'error' | 'silent'
});Lifecycle Hooks
kaput.on({
onShutdownStart: (event) => {
console.log(`Shutdown started: ${event.signal}`);
},
onShutdownComplete: () => {
console.log('Shutdown complete');
},
});Performance
kaput adds negligible overhead to your shutdown sequence:
| Operation | Avg Time | Throughput | |-----------|----------|------------| | Empty shutdown | 0.02ms | 45,000 ops/sec | | Single resource | 0.03ms | 38,000 ops/sec | | 10 resources | 0.05ms | 21,000 ops/sec | | 100 resources | 0.21ms | 4,700 ops/sec | | Registration (1000x) | 0.66ms | - |
- Memory overhead: ~500 bytes per registered resource
- Test coverage: 350+ tests including adversarial edge cases
- Zero dependencies: No external runtime dependencies
The actual shutdown time is dominated by your resources (database connections, HTTP draining), not kaput's coordination.
Documentation
Full documentation at kaput.dev/docs
License
MIT
