Lifecycle hooks and graceful shutdown
Nest fires init hooks bottom-up (onModuleInit then onApplicationBootstrap) and shutdown hooks in reverse (onModuleDestroy, beforeApplicationShutdown, onApplicationShutdown) — but only if you call enableShutdownHooks() so it listens for SIGTERM.
A deploy goes out at 14:02. Kubernetes rolls the pods, and for ninety seconds your error dashboard lights up: ECONNRESET, half-written rows, a spike of 502s from the load balancer. The new pods are healthy; it’s the old ones that are misbehaving on the way out. You open the service and find a PrismaService whose $connect() lives in the constructor and whose $disconnect() lives nowhere — and a main.ts that never calls enableShutdownHooks(). So when k8s sends SIGTERM, Node just dies: in-flight requests are severed mid-write and the connection pool is never drained. Nest had a hook for every one of those steps. Nobody wired them up.
The lifecycle has two phases, each with a fixed order
A Nest application has a birth and a death, and both are ordered events you can hook into. Knowing the order is the whole game: it tells you when your injected dependencies are guaranteed ready, and when it is safe to close them.
Init runs bottom-up. When you call app.listen() (or app.init()), Nest resolves the dependency graph and then fires, per provider/module:
onModuleInit()— called for a module after that module’s own dependencies are resolved. This is the first moment your injected providers are guaranteed constructed and wired.onApplicationBootstrap()— called after every module has finished initializing, just before the server starts listening. This is the first moment the whole app is ready, including providers from other modules.
Teardown runs in reverse. On app.close() or a termination signal, Nest unwinds in reverse dependency order:
onModuleDestroy()— the termination signal has arrived; begin tearing down.beforeApplicationShutdown(signal)— runs after allonModuleDestroy()handlers, before connections are closed; receives the signal ("SIGTERM","SIGINT", …).onApplicationShutdown(signal)— runs after connections are closed; the last breath, where you release pools, queue consumers, and clients.
Reverse order matters: a module that depends on the database is torn down before the database module, so it never tries to use a connection that’s already gone. Each interface contributes one method — OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown — and you implement only the ones you need. Async hooks are awaited: returning a Promise pauses the sequence until it resolves.
onModuleInit vs the constructor
A provider’s constructor runs at instantiation, while Nest is still wiring the graph. At that point your injected dependencies exist (they were constructed first), but any async setup they need — a TCP connection, a primed cache, a fetched config — has not run. Doing real I/O in the constructor is a trap: constructors can’t be async, so you either block synchronously or fire-and-forget a floating promise, and you have no clean place to handle failure.
onModuleInit() is the answer. When you need to open a connection, prime a cache, or verify that an external service is reachable before traffic arrives, this is where that code belongs. It runs once, right after the module’s dependencies resolve, it can be async, and Nest awaits it before moving on. That makes it the correct home for warm-up that needs ready dependencies:
import { Injectable, OnModuleInit, OnApplicationShutdown } from '@nestjs/common';
import { Pool } from 'pg';
@Injectable()
export class DatabaseService implements OnModuleInit, OnApplicationShutdown {
private pool!: Pool;
constructor(private readonly config: ConfigService) {
// constructor: deps exist, but do NO async I/O here
this.pool = new Pool({ connectionString: this.config.get('DB_URL') });
}
async onModuleInit(): Promise<void> {
// warm-up: dependencies are ready, await is honored
await this.pool.query('SELECT 1'); // prove the pool connects before we serve traffic
}
async onApplicationShutdown(signal: string): Promise<void> {
// drain: connections close last, after in-flight work is done
console.log(`shutting down on ${signal}`);
await this.pool.end(); // close every client in the pool
}
}The split is deliberate: construct cheaply, connect in onModuleInit, and release in onApplicationShutdown. If the warm-up query throws, Nest aborts startup with a clear error instead of booting a service that can’t reach its database. Use onApplicationBootstrap instead of onModuleInit only when the warm-up depends on another module being fully up — e.g. priming a cache from a service defined in a different module that must finish its own onModuleInit first.
Graceful shutdown, done right
Shutdown hooks do nothing unless you opt in. app.enableShutdownHooks() registers process listeners for SIGTERM/SIGINT so Nest can fire the teardown sequence; it is off by default because attaching signal listeners has a cost and Nest won’t impose it silently.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
// also force-close idle Keep-Alive sockets so the process can actually exit
forceCloseConnections: true,
});
app.enableShutdownHooks(); // REQUIRED: otherwise SIGTERM just kills the process
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();Now the actual sequence on SIGTERM: stop accepting new work, drain in-flight requests, then close resources. Nest’s HTTP adapter stops the server and waits for open responses to finish before resolving the close — but long-lived Connection: Keep-Alive sockets can keep the process hostage, which is why forceCloseConnections exists. Once requests are drained, your onApplicationShutdown closes the DB pool, queue consumers, and Kafka clients — in reverse dependency order, so nothing closes a resource another provider still needs.
| Hook | Phase | Fires when | Use it for |
|---|---|---|---|
| onModuleInit | Init (1st) | After this module’s deps resolve | Connect, prime cache, warm-up needing injected deps |
| onApplicationBootstrap | Init (2nd) | After ALL modules initialized, before listen | Cross-module warm-up that needs the whole app up |
| onModuleDestroy | Down (1st) | Signal arrived; teardown begins | Stop consumers, flush buffers |
| beforeApplicationShutdown | Down (2nd) | After onModuleDestroy, before connections close | Last-chance work needing live connections; reads signal |
| onApplicationShutdown | Down (3rd) | After connections close | Release pools, clients, sockets; reads signal |
▸Why this works
Why does Kubernetes make this urgent? On a rolling update, k8s sends SIGTERM and waits terminationGracePeriodSeconds (default 30s) before sending an unstoppable SIGKILL. If you never enabled shutdown hooks, nothing drains and SIGKILL cuts every connection. But there’s a subtler race: k8s removes the pod from the Service endpoints and sends SIGTERM roughly in parallel, so for a moment the load balancer may still route new requests to a terminating pod. The senior fix is to flip the readiness probe to failing first — the pod leaves the load-balancer rotation, traffic stops arriving, and only then do you drain and close. This is why graceful shutdown is a networking concern as much as a lifecycle one.
A provider needs to open a database connection at startup. Where should the connect() call live so the connection is ready before traffic arrives and failures abort the boot cleanly?
In what order do Nest's init and shutdown hooks fire?
Your onApplicationShutdown never fires when Kubernetes sends SIGTERM. What's the most likely cause?
- 01State the full Nest lifecycle order for both init and shutdown, and explain why teardown runs in reverse.
- 02Why is enableShutdownHooks() required, and how does graceful shutdown tie into Kubernetes?
A Nest application has an ordered birth and death you can hook into. Init runs bottom-up: onModuleInit fires for each module the moment its own dependencies resolve — the correct, async-aware home for warm-up like opening a connection or priming a cache — and onApplicationBootstrap fires once after every module is up, for cross-module warm-up that needs the whole app ready. The constructor is the wrong place for that work because it can’t be async and runs while the graph is still wiring. Teardown runs in reverse dependency order: onModuleDestroy, then beforeApplicationShutdown(signal) before connections close, then onApplicationShutdown(signal) after they close — the slot to release pools, queue consumers, and clients. None of the shutdown hooks fire unless you call app.enableShutdownHooks(), which is opt-in because it attaches SIGTERM/SIGINT listeners; the cost is why Nest won’t do it silently. Graceful shutdown done right is: stop accepting new work, drain in-flight requests (forceCloseConnections for stubborn Keep-Alive sockets), then close resources — and in Kubernetes you flip the readiness probe to failing first so the load balancer pulls the pod before you drain, all within terminationGracePeriodSeconds before SIGKILL. The classic bug is connecting in the constructor and closing nowhere, with shutdown hooks never enabled, so a deploy severs requests and leaks connections. Now when you see a rolling-update spike on the error dashboard, you know to look for a missing enableShutdownHooks() and a connect() that lives in the constructor — and you know exactly how to fix both.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.