Zero-config graceful shutdown for Node.js
npm install @joint-ops/kaput> Zero-config graceful shutdown for Node.js



Graceful shutdown in Node.js is harder than it looks. Here's what most apps do:
``typescript
// 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...
`
`typescript`
import 'kaput';
That's it. kaput handles everything automatically.
- 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
`bash`
npm install @joint-ops/kaput
Add one line at the top of your entry file:
`typescript
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 cleanly
`
When you need more control:
`typescript
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);
`
Return 503 during shutdown so load balancers stop sending traffic:
`typescript
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' });
});
`
| 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 |
`typescript
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'
});
`
`typescriptShutdown started: ${event.signal}
kaput.on({
onShutdownStart: (event) => {
console.log();``
},
onShutdownComplete: () => {
console.log('Shutdown complete');
},
});
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.
Full documentation at kaput.dev/docs
- Getting Started
- Express Guide
- Kubernetes Guide
- API Reference
MIT