Implementing a graceful shutdown for your Fastify application prevents data loss and keeps your microservices reliable when containers stop, deployments roll out, or the OS shuts down.
Understanding Graceful Shutdown in Fastify
A graceful shutdown allows a server to finish processing in‑flight requests, close database connections, and release resources before the process exits. Fastify, built on Node.js, gives you hooks and lifecycle methods that make this pattern straightforward.
Prerequisites – Node.js 20, Fastify 5, and TypeScript 5
Before coding, ensure you have the following installed:
- Node.js 20 (LTS)
- Fastify ^5.0
- TypeScript ^5.0
- ts-node for on‑the‑fly execution
Run npm init -y && npm i fastify typescript ts-node @types/node to set up the baseline.
Setting Up a Minimal Fastify Project
Create src/server.ts with a simple route:
import Fastify from 'fastify';
const app = Fastify({ logger: true });
app.get('/', async (request, reply) => {
return { hello: 'world' };
});
export default app;
Start the server in src/index.ts:
import app from './server';
const start = async () => {
try {
await app.listen({ port: 3000 });
console.log('🚀 Server running on http://localhost:3000');
} catch (err) {
app.log.error(err);
process.exit(1);
}
};
start();
Capturing Process Signals (SIGINT & SIGTERM)
Node.js emits signals when the OS requests termination. Listen for them early in the lifecycle:
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
These handlers will invoke a shared shutdown function you will define next.
Implementing a Graceful Shutdown Hook
The core of the pattern is an async function that:
- Stops accepting new connections with
app.close(). - Waits for all active requests to finish.
- Closes external resources (DB pools, message queues, etc.).
- Exits the process with a suitable code.
Example implementation:
import type { FastifyInstance } from 'fastify';
const gracefulShutdown = async (app: FastifyInstance, signal: string) => {
console.log(`Received ${signal}. Starting graceful shutdown...`);
try {
// Stop accepting new requests
await app.close();
console.log('✅ Fastify stopped accepting new connections');
// Add custom cleanup here, e.g., DB.disconnect()
// await db.disconnect();
console.log('🛑 Cleanup completed. Exiting now.');
process.exit(0);
} catch (err) {
console.error('❌ Error during shutdown:', err);
process.exit(1);
}
};
function shutdown(signal: string) {
// Import the already‑started app instance
import('./server').then(({ default: app }) => gracefulShutdown(app, signal));
}
Integrating the Hook into Fastify’s Lifecycle
Fastify offers the onClose hook, perfect for tying custom logic to the server’s close event:
app.addHook('onClose', async (instance, done) => {
// Example: gracefully close a Prisma client
// await prisma.$disconnect();
done();
});
By registering this hook before app.listen(), any app.close() call will automatically execute your cleanup code.
Testing the Shutdown Flow
Run the server with npx ts-node src/index.ts and open a few HTTP requests (e.g., via curl http://localhost:3000). Then send a termination signal:
# In another terminal
kill -SIGINT $(pgrep -f 'ts-node')
You should see the console log messages from the shutdown hook, and the process exits only after pending requests are resolved.
Common Pitfalls and Best Practices
Never call process.exit() immediately. Doing so aborts ongoing I/O, causing data loss. Always await app.close() first.
Other considerations:
- Keep‑alive sockets: Set
server.keepAliveTimeoutto a low value (e.g., 5 s) if you expect many idle connections. - Database pools: Drain connections with
pool.end()or equivalent client method. - Message queues: Flush pending messages before terminating.
- Health checks: Return
503from a/healthzendpoint once shutdown begins, so load balancers stop routing traffic.
Conclusion
Adding a graceful shutdown to a Fastify server running on Node.js 20 and TypeScript is a few lines of code but delivers immense reliability gains. Capture signals, call app.close(), clean up external resources, and let the event loop finish naturally. Implement these steps today to ensure zero‑downtime deployments and clean exits in production.