Graceful Shutdown
When an orchestrator redeploys your app it sends a termination signal and gives the process a few seconds to exit before forcibly killing it. If your NestJS app exits immediately, in-flight requests get dropped, database transactions are abandoned, and message consumers leave records half-processed. Graceful shutdown is the discipline of reacting to that signal: stop accepting new work, let current requests finish, and close every connection cleanly. NestJS makes this manageable with lifecycle hooks that fire on SIGTERM and SIGINT.
How NestJS shutdown hooks work
Nest does not listen for process termination signals by default, because attaching listeners has a small performance cost and not every app needs them. You opt in by calling app.enableShutdownHooks(). Once enabled, when the process receives SIGTERM, SIGINT, or another configured signal, Nest walks the dependency graph in reverse and invokes lifecycle methods on every provider and module that implements them.
Two interfaces matter for cleanup:
| Hook | Interface | Fires when | Typical use |
|---|---|---|---|
beforeApplicationShutdown | BeforeApplicationShutdown | Right after the signal, before connections close | Stop accepting new jobs, flush buffers |
onApplicationShutdown | OnApplicationShutdown | After the above, receives the signal name | Close DB pools, disconnect brokers |
Both hooks can return a Promise, and Nest awaits them in sequence, so you can perform genuinely async teardown.
Enabling hooks in main.ts
Enable shutdown hooks during bootstrap. This single call wires up the signal listeners for the whole application.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
// Listen for SIGTERM / SIGINT and run lifecycle hooks
app.enableShutdownHooks();
await app.listen(process.env.PORT ?? 3000, '0.0.0.0');
}
bootstrap();
Containers running
nodeas PID 1 do not always forwardSIGTERM. Start the container withdocker run --init(or addtini) so the signal actually reaches your process and the hooks fire. Without this, Kubernetes will SIGKILL the pod after the grace period and your cleanup never runs.
Closing database connections and queues
Implement OnApplicationShutdown in the providers that own external resources. The hook receives the signal name, which is useful for logging. Below, a Prisma-backed service disconnects its pool and a queue service drains and closes its connection.
// src/database/prisma.service.ts
import { Injectable, OnApplicationShutdown, Logger } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnApplicationShutdown
{
private readonly logger = new Logger(PrismaService.name);
async onApplicationShutdown(signal?: string): Promise<void> {
this.logger.log(`Closing database connection (${signal})`);
await this.$disconnect();
}
}
// src/queue/consumer.service.ts
import {
Injectable,
BeforeApplicationShutdown,
OnApplicationShutdown,
Logger,
} from '@nestjs/common';
import { Queue, Worker } from 'bullmq';
@Injectable()
export class ConsumerService
implements BeforeApplicationShutdown, OnApplicationShutdown
{
private readonly logger = new Logger(ConsumerService.name);
constructor(
private readonly worker: Worker,
private readonly queue: Queue,
) {}
// Stop pulling new jobs before connections close
async beforeApplicationShutdown(): Promise<void> {
this.logger.log('Pausing worker; no new jobs will be picked up');
await this.worker.pause(false); // wait for the active job to settle
}
// Release the underlying Redis connections
async onApplicationShutdown(signal?: string): Promise<void> {
this.logger.log(`Closing queue connections (${signal})`);
await this.worker.close();
await this.queue.close();
}
}
Because beforeApplicationShutdown runs first, the worker stops claiming new jobs while existing ones complete; then onApplicationShutdown tears down the Redis sockets.
Draining in-flight HTTP requests
app.enableShutdownHooks() triggers Nest’s app.close() flow, which stops the HTTP server from accepting new connections and waits for active requests to finish before resolving lifecycle hooks. This is the natural drain mechanism. The risk is the gap between the orchestrator removing the pod from its load balancer and the server actually refusing connections — a short delay before shutdown lets the load balancer catch up so no new request lands on a closing process.
// src/health/shutdown.service.ts
import { Injectable, BeforeApplicationShutdown } from '@nestjs/common';
@Injectable()
export class ShutdownService implements BeforeApplicationShutdown {
// Give the load balancer time to stop routing to this instance
async beforeApplicationShutdown(signal?: string): Promise<void> {
if (signal === 'SIGTERM') {
await new Promise((resolve) => setTimeout(resolve, 5_000));
}
}
}
Pair this with a readiness probe that starts failing the moment a shutdown begins, so Kubernetes removes the pod from the Endpoints list. The combination — failing readiness, a short sleep, then the natural request drain — eliminates dropped traffic during rolling redeploys.
Verifying it works
Run the app and send it SIGTERM with a request in flight; the hooks log their teardown in reverse provider order.
node dist/main.js &
kill -TERM %1
Output:
[Nest] 1 - 06/14/2026, 9:30:11 AM LOG [ShutdownService] draining for 5000ms
[Nest] 1 - 06/14/2026, 9:30:16 AM LOG [ConsumerService] Pausing worker; no new jobs will be picked up
[Nest] 1 - 06/14/2026, 9:30:16 AM LOG [ConsumerService] Closing queue connections (SIGTERM)
[Nest] 1 - 06/14/2026, 9:30:16 AM LOG [PrismaService] Closing database connection (SIGTERM)
[Nest] 1 - 06/14/2026, 9:30:16 AM LOG [NestApplication] Nest application shut down gracefully
Tuning the orchestrator grace period
Cleanup only helps if the platform waits long enough. Kubernetes defaults terminationGracePeriodSeconds to 30, which must comfortably exceed your drain delay plus the slowest hook. If a long-running request can take 20 seconds, set the grace period above that or it gets SIGKILLed mid-flight.
spec:
terminationGracePeriodSeconds: 45
containers:
- name: api
lifecycle:
preStop:
exec:
command: ["sleep", "5"]
Best Practices
- Call
app.enableShutdownHooks()exactly once during bootstrap; it is a no-op to call more than once but adds clutter. - Start the container with
--initortinisoSIGTERMactually reaches Node as PID 1. - Use
beforeApplicationShutdownto stop accepting new work andonApplicationShutdownto close connections, in that order. - Make readiness probes fail at shutdown and add a short drain delay so the load balancer stops routing first.
- Set
terminationGracePeriodSecondslarger than your drain delay plus the slowest in-flight request. - Always
awaitasync teardown (DB$disconnect, brokerclose) so connections close cleanly instead of being severed. - Keep hooks fast and idempotent — they may run during crash-loop restarts as well as planned redeploys.