Lifecycle Hooks
Every NestJS application moves through a well-defined lifecycle: modules are instantiated, dependencies are resolved, the app starts listening, and eventually it shuts down. Lifecycle hooks let you tap into these moments to open database connections, warm caches, register message consumers, and—critically—release those resources cleanly on exit. Using them correctly is the difference between a service that restarts gracefully and one that leaks connections or drops in-flight requests.
The lifecycle in order
Nest fires hooks in a deterministic sequence. On startup it initializes each module, then bootstraps the application; on shutdown it reverses direction and tears everything down. The table below lists the hooks in execution order.
| Phase | Interface | Method | When it runs |
|---|---|---|---|
| Startup | OnModuleInit | onModuleInit() | After a host module’s dependencies are resolved |
| Startup | OnApplicationBootstrap | onApplicationBootstrap() | After all modules are initialized, before listening |
| Shutdown | OnModuleDestroy | onModuleDestroy() | After a termination signal is received |
| Shutdown | BeforeApplicationShutdown | beforeApplicationShutdown(signal?) | After all onModuleDestroy handlers complete |
| Shutdown | OnApplicationShutdown | onApplicationShutdown(signal?) | After connections close, just before exit |
Within a single phase, hooks respect module dependency order: a provider in an imported module runs its startup hook before the importing module, and the reverse on shutdown.
Startup hooks
OnModuleInit is the right place for per-module setup that depends on injected providers being ready—establishing a connection pool, for example. OnApplicationBootstrap runs once every module is wired up, so it suits cross-cutting work like subscribing to events or kicking off a scheduler.
Each hook may return a Promise, and Nest awaits it before proceeding. This guarantees your app is fully ready before it accepts traffic.
import { Injectable, Logger, OnModuleInit, OnApplicationBootstrap } from '@nestjs/common';
import { DataSource } from 'typeorm';
@Injectable()
export class DatabaseService implements OnModuleInit, OnApplicationBootstrap {
private readonly logger = new Logger(DatabaseService.name);
constructor(private readonly dataSource: DataSource) {}
async onModuleInit(): Promise<void> {
if (!this.dataSource.isInitialized) {
await this.dataSource.initialize();
}
this.logger.log('Database connection established');
}
async onApplicationBootstrap(): Promise<void> {
const count = await this.dataSource.query('SELECT count(*) FROM migrations');
this.logger.log(`Bootstrap complete: ${count[0].count} migrations applied`);
}
}
Output:
[Nest] LOG [DatabaseService] Database connection established
[Nest] LOG [DatabaseService] Bootstrap complete: 14 migrations applied
[Nest] LOG [NestApplication] Nest application successfully started
Shutdown hooks
Shutdown hooks only fire if you explicitly opt in by calling enableShutdownHooks(). This is a deliberate guard: attaching SIGTERM/SIGINT listeners has a small cost and side effects, so Nest does not do it by default.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(); // required for OnModuleDestroy & friends
await app.listen(3000);
}
bootstrap();
OnModuleDestroy is where you stop accepting new work and close resources owned by that module. BeforeApplicationShutdown runs after every module has destroyed, making it ideal for tasks that need all providers still alive but must run last. OnApplicationShutdown receives the terminating signal and is the final chance to flush logs or notify a service registry.
import {
Injectable,
Logger,
OnModuleDestroy,
BeforeApplicationShutdown,
OnApplicationShutdown,
} from '@nestjs/common';
import { Kafka, Consumer } from 'kafkajs';
@Injectable()
export class EventConsumer
implements OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown
{
private readonly logger = new Logger(EventConsumer.name);
private consumer: Consumer = new Kafka({ brokers: ['localhost:9092'] }).consumer({
groupId: 'orders',
});
async onModuleDestroy(): Promise<void> {
await this.consumer.stop();
this.logger.log('Stopped consuming new messages');
}
async beforeApplicationShutdown(signal?: string): Promise<void> {
this.logger.log(`Draining in-flight work (signal: ${signal})`);
}
async onApplicationShutdown(signal?: string): Promise<void> {
await this.consumer.disconnect();
this.logger.log(`Consumer disconnected, exiting on ${signal}`);
}
}
Output:
[Nest] LOG [EventConsumer] Stopped consuming new messages
[Nest] LOG [EventConsumer] Draining in-flight work (signal: SIGTERM)
[Nest] LOG [EventConsumer] Consumer disconnected, exiting on SIGTERM
Graceful shutdown in containers
In Kubernetes and Docker, the orchestrator sends SIGTERM and then waits a grace period before sending SIGKILL. A graceful service stops health checks first, lets the load balancer drain it, finishes outstanding requests, and only then closes connections. With shutdown hooks enabled, Nest awaits each async handler, so your cleanup completes within the grace window.
@Injectable()
export class HealthState implements OnModuleDestroy {
private healthy = true;
isHealthy(): boolean {
return this.healthy;
}
onModuleDestroy(): void {
// Fail readiness probes so the LB stops routing traffic here.
this.healthy = false;
}
}
Set
terminationGracePeriodSecondsin your pod spec to comfortably exceed your longest cleanup task. If a hook hangs, the process is killed mid-shutdown and you lose the graceful guarantees.
Best practices
- Call
app.enableShutdownHooks()exactly once inbootstrap(); without it, no shutdown hook will ever fire. - Keep hooks idempotent and fast—long-running cleanup risks exceeding the orchestrator grace period and getting
SIGKILLed. - Use
OnModuleInitfor module-local setup andOnApplicationBootstrapfor work that needs the entire app wired up. - Always
awaitasync work inside hooks; returning a promise lets Nest sequence startup and teardown correctly. - Close resources in the hook of the module that owns them so dependency ordering tears things down in the right sequence.
- Flip readiness/health state to unhealthy first on shutdown so load balancers drain traffic before connections close.
- Avoid heavy logic in
onApplicationShutdown—treat it as a last-chance flush, since providers may already be partially destroyed.