When a NestJS application is running inside a Docker container or Kubernetes pod, the container doesn't simply vanish—it receives a termination signal that gives your app a few seconds to wrap up. Graceful shutdown in NestJS means intercepting that signal, completing in-flight requests and background jobs, and closing database and cache connections cleanly before exiting. Without it, you'll lose data, leave orphaned connections, and watch your logs fill with errors.
Why graceful shutdown matters
I've debugged production incidents where a load balancer sent a SIGTERM signal to a container, and the process died mid-transaction. Database connections were left hanging, Redis subscribers were abruptly cut off, and job queues had partially processed messages. The real cost isn't just technical debt—it's data consistency and customer trust.
Container orchestrators like Kubernetes send a graceful shutdown signal before forcefully killing a process. By default, you have around 30 seconds (configurable via terminationGracePeriodSeconds) to finish work. If your app ignores that signal, the orchestrator kills it hard, and everything breaks. NestJS provides built-in hooks to handle this pattern elegantly.
Enable shutdown hooks in main.ts
The foundation of graceful shutdown in NestJS is calling app.enableShutdownHooks() in your bootstrap code. This tells NestJS to listen for system signals (SIGTERM, SIGINT) and invoke lifecycle hooks on your providers and modules.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable graceful shutdown
app.enableShutdownHooks();
await app.listen(3000);
console.log('Application is running');
}
bootstrap();That single line unlocks the ability to handle shutdown events. Without it, NestJS won't trigger the lifecycle hooks I'm about to show you. Always add this to any production application.
Implement onApplicationShutdown in providers
Once shutdown hooks are enabled, your services and other providers can implement the OnApplicationShutdown interface. This lifecycle hook fires when the application receives a termination signal, giving each provider a chance to clean up.
Here's a practical example with a hypothetical job processor service:
import { Injectable, OnApplicationShutdown } from '@nestjs/common';
import Bull from 'bull';
@Injectable()
export class JobProcessorService implements OnApplicationShutdown {
private jobQueue: Bull.Queue;
private isShuttingDown = false;
constructor() {
this.jobQueue = new Bull('email-queue', {
redis: { host: 'localhost', port: 6379 },
});
}
async processJobs() {
this.jobQueue.process(async (job) => {
if (this.isShuttingDown) {
throw new Error('Service is shutting down');
}
// Process email job
return { success: true };
});
}
async onApplicationShutdown(signal: string) {
console.log(`Received signal: ${signal}`);
this.isShuttingDown = true;
// Wait for current jobs to finish
await this.jobQueue.close();
console.log('Job queue closed gracefully');
}
}
The onApplicationShutdown method receives a signal parameter (e.g., SIGTERM) and runs when the application shuts down. By setting a flag like isShuttingDown and checking it in your processing logic, you prevent new work from starting while allowing in-flight work to complete.
Close database connections gracefully
Database connections are critical. If you're using TypeORM, Prisma, or any ORM, you need to close them during shutdown to avoid connection leaks and ensure pending transactions are committed or rolled back properly.
Here's a TypeORM example:
import { Injectable, OnApplicationShutdown } from '@nestjs/common';
import { DataSource } from 'typeorm';
@Injectable()
export class DatabaseService implements OnApplicationShutdown {
constructor(private dataSource: DataSource) {}
async onApplicationShutdown(signal: string) {
console.log(`Database: Shutting down gracefully (${signal})`);
if (this.dataSource.isInitialized) {
await this.dataSource.destroy();
console.log('Database connection closed');
}
}
}
When you use NestJS with TypeORM as a provider, the connection is typically already managed by the framework. However, for custom database clients or connection pools, you should wrap the connection logic in a service and implement OnApplicationShutdown explicitly. Always check if the connection is active before closing to avoid double-close errors.
Stop consuming new messages from Redis and queues
If your application subscribes to Redis channels or consumes messages from job queues (like Bull, RabbitMQ, or Kafka), the shutdown sequence matters. You want to stop accepting new work immediately, then wait for in-flight jobs to finish.
Here's a Redis subscriber service pattern:
import { Injectable, OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class RedisSubscriberService implements OnModuleInit, OnApplicationShutdown {
private redis: Redis;
private isAcceptingMessages = true;
constructor() {
this.redis = new Redis({
host: 'localhost',
port: 6379,
});
}
async onModuleInit() {
this.redis.subscribe('notifications', async (err, count) => {
if (err) console.error('Redis subscribe error:', err);
else console.log(`Subscribed to ${count} channel(s)`);
});
this.redis.on('message', (channel, message) => {
if (!this.isAcceptingMessages) {
console.log('Rejecting new messages during shutdown');
return;
}
// Process message
console.log(`Message on ${channel}: ${message}`);
});
}
async onApplicationShutdown(signal: string) {
console.log(`Redis subscriber: Graceful shutdown (${signal})`);
this.isAcceptingMessages = false;
// Unsubscribe from all channels
await this.redis.unsubscribe();
// Close Redis connection
await this.redis.quit();
console.log('Redis subscriber closed');
}
}
The key insight here is the isAcceptingMessages flag. As soon as shutdown begins, you flip this flag to reject new incoming messages. Any messages already being processed will finish, but no new ones are accepted. Then you cleanly close the Redis connection.
For job queues like Bull, you need a similar pattern but with an added layer: pause the queue before closing it:
async onApplicationShutdown(signal: string) {
console.log(`Job consumer: Graceful shutdown (${signal})`);
// Stop consuming new jobs
await this.queue.pause();
// Wait briefly for in-flight jobs (configure timeout based on your needs)
await new Promise(resolve => setTimeout(resolve, 5000));
// Then close
await this.queue.close();
console.log('Job queue closed');
}
Orchestrate shutdown sequence
When multiple providers implement OnApplicationShutdown, NestJS calls them in reverse dependency order. If Service A depends on Service B, B shuts down first. However, you can't always rely on the framework to get the order right, especially with circular dependencies or independent services.
I recommend creating a dedicated shutdown coordinator service that explicitly manages the sequence:
import { Injectable, OnApplicationShutdown } from '@nestjs/common';
import { JobProcessorService } from './job-processor.service';
import { RedisSubscriberService } from './redis-subscriber.service';
import { DatabaseService } from './database.service';
@Injectable()
export class ShutdownCoordinatorService implements OnApplicationShutdown {
constructor(
private jobProcessor: JobProcessorService,
private redisSubscriber: RedisSubscriberService,
private database: DatabaseService,
) {}
async onApplicationShutdown(signal: string) {
console.log(`\n=== Graceful Shutdown Started (${signal}) ===\n`);
// Step 1: Stop accepting new work
console.log('Step 1: Stopping new work intake...');
await this.jobProcessor.stopAcceptingJobs();
await this.redisSubscriber.stopAcceptingMessages();
// Step 2: Wait for in-flight work
console.log('Step 2: Waiting for in-flight jobs to complete...');
await this.sleep(3000); // Adjust based on typical job duration
// Step 3: Close external connections
console.log('Step 3: Closing external connections...');
await this.redisSubscriber.onApplicationShutdown(signal);
await this.jobProcessor.onApplicationShutdown(signal);
// Step 4: Close database last
console.log('Step 4: Closing database...');
await this.database.onApplicationShutdown(signal);
console.log('=== Graceful Shutdown Complete ===\n');
}
private sleep(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
By centralizing the shutdown logic, you gain explicit control over timing and ordering. This pattern works especially well in complex applications with many external dependencies.
Common mistakes
- Forgetting to call enableShutdownHooks(): Your lifecycle hooks won't fire at all. The application will exit immediately on signal, losing data.
- Not stopping new work intake first: If you close connections while still accepting requests, you'll get connection errors and failed operations. Always flip the "accepting work" flag before shutting down resources.
- Ignoring the timeout: Kubernetes and container platforms have a hard timeout (default 30 seconds). If your shutdown logic takes longer, the process gets killed forcefully. Test your shutdown time and set
terminationGracePeriodSecondsaccordingly. - Closing dependencies in the wrong order: If you close the database before job processing completes, in-flight jobs will fail. Plan your shutdown sequence carefully or use a coordinator service.
- Not handling SIGINT for local development: When you press Ctrl+C in the terminal, SIGINT is sent. Ensure your shutdown logic handles both SIGTERM (from orchestrators) and SIGINT (from terminals).
Key takeaways
- Always call
app.enableShutdownHooks()in your bootstrap function to enable graceful shutdown handling. - Implement
OnApplicationShutdownon any service that manages external resources: databases, caches, queues, or message brokers. - Stop accepting new work immediately when shutdown begins, then wait for in-flight work to complete before closing connections.
- For complex applications with multiple external dependencies, use a coordinator service to enforce a strict shutdown sequence.
- Test your shutdown logic locally with SIGTERM signals and verify that in-flight requests complete before the process exits.
- Set an appropriate
terminationGracePeriodSecondsin Kubernetes to give your application enough time to shut down.






Comments
Loading comments…