NestJS Module 4 – Modules, dynamic modules and lifecycle
Lifecycle hooks and shutdown signals
The order of Nest's five lifecycle hooks across modules, how enableShutdownHooks() lets SIGTERM run them, and how to stop workers and servers cleanly.
What you will learn
- Order onModuleInit, onApplicationBootstrap and the three shutdown hooks across the modules of an app
- Configure shutdown hooks so that SIGTERM runs the cleanup code, and predict how the process ends
- Handle SIGTERM in a worker and an HTTP server without losing the job or the requests in flight
- Limit a shutdown hook with a deadline, and diagnose a hook that throws or never settles
Before you start
What you’ll learn · Before you start
What you will learn
- Order onModuleInit, onApplicationBootstrap and the three shutdown hooks across the modules of an app
- Configure shutdown hooks so that SIGTERM runs the cleanup code, and predict how the process ends
- Handle SIGTERM in a worker and an HTTP server without losing the job or the requests in flight
- Limit a shutdown hook with a deadline, and diagnose a hook that throws or never settles
Before you start
On this page
Every deploy sends TiffinTrail’s kitchen worker SIGTERM, often while it is 100 ms into a 300 ms job. With Nest’s defaults the process ends at once, with exit status 143: the job is left half done, and the queue and the database pool are never closed.
Nest has five lifecycle hook: A method such as onModuleInit() or onApplicationShutdown() that Nest calls on providers, controllers and module classes at a fixed point of start-up or shutdown, and awaits when it returns a promise. for start-up and shutdown, and shutdown signals reach the last three only when the app asks for them. This lesson runs the hooks in four modules to see their order, sends SIGTERM to a worker with and without shutdown hooks, then drains a worker and an HTTP server and bounds the hooks that hang.
Five hooks run in a fixed order, module by module
A provider, a controller or a module class takes part in its app’s lifecycle by having one of these methods, each with
an interface of the same name in @nestjs/common:
onModuleInit()runs at start-up, once the class’s module has resolved its dependencies.onApplicationBootstrap()runs once every module has been initialized, before a server starts listening.onModuleDestroy()is the first shutdown hook, called when the app starts closing.beforeApplicationShutdown(signal)runs after everyonModuleDestroy()has settled, before connections close.onApplicationShutdown(signal)is the last, called once the server’s connections have closed.
Nest awaits a hook that returns a promise, so start-up waits for an async onModuleInit(). Request-scoped classes get
none of the five: the documentation says they are not tied to the app’s lifecycle. The order across modules is what
people guess wrong. The program gives four modules a provider each with all five hooks, plus a second provider in
CacheModule that needs the first, and prints the calls:
import { session } from '../session.mjs';
// Four modules (a global ConfigModule, DbModule, CacheModule with two providers, AppModule), every provider with all
// five lifecycle hooks: the order of the calls at start-up and at app.close().
session(`
npx tsc -p tsconfig.json
node dist/hook-order.js
`); Output
$ npx tsc -p tsconfig.json $ node dist/hook-order.js createApplicationContext() ran the start-up hooks: onModuleInit: AppConfig, DbPool, CacheClient, CacheWarmer, OrdersService onApplicationBootstrap: AppConfig, DbPool, CacheClient, CacheWarmer, OrdersService app.close() ran the shutdown hooks: onModuleDestroy: OrdersService, CacheWarmer, CacheClient, DbPool, AppConfig beforeApplicationShutdown(undefined): OrdersService, CacheWarmer, CacheClient, DbPool, AppConfig onApplicationShutdown(undefined): OrdersService, CacheWarmer, CacheClient, DbPool, AppConfig
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node hook-order.mjs
Every hook ran module by module, and the documentation states the rule: the modules farthest from the root module along the imports go first, global modules count as the farthest, and the root module goes last. Shutdown runs in the reverse order.
New in Nest 12: hooks follow the dependencies inside a module
The migration guide notes that lifecycle hooks are now called by component hierarchy level. In the run, CacheWarmer
is listed before CacheClient but needs it, and its onModuleInit() came second; at shutdown it came first. An app
upgraded from Nest 11 that relied on the order of its providers list may see its hooks in a new order.
Step through the run, from start-up to app.close(), one module at a time:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Distances from the root. Nest measures each module's distance from the root module along the imports. AppModule is 1, CacheModule 2 and DbModule 3; ConfigModule is global, which Nest counts as farther than any other module.
- onModuleInit, global first. At start-up Nest calls onModuleInit() module by module, the farthest first, and awaits each module before the next. The global ConfigModule comes first, so AppConfig was the first line of the run.
- Then the deepest import. DbModule, at distance 3, comes next. DbPool's hook ran before any class that depends on it.
- Inside a module, by dependencies. In CacheModule, CacheWarmer is listed first but needs CacheClient. Nest 12 calls a module's hooks by each provider's depth in its dependencies, so CacheClient ran before CacheWarmer.
- The root module last. AppModule comes last, with OrdersService. Then onApplicationBootstrap() runs over the modules in the same order, and only after it does createApplicationContext() return.
- app.close() reverses it. app.close() calls onModuleDestroy(), beforeApplicationShutdown() and onApplicationShutdown() in the reverse order, the root first and the global module last, so a class shuts down before what it depends on.
- The signal the hooks receive. The last two hooks receive the signal that started the shutdown. After app.close() there is none, so the run printed undefined; after SIGTERM with shutdown hooks enabled, they receive SIGTERM.
Key idea
Start-up hooks run from the farthest module to the root, global modules first; shutdown hooks run from the root to the farthest; and inside a module a provider’s hooks follow its dependencies.
Checkpoint
Check: The order of the five hooks across modules
Shutdown signals run the hooks only after enableShutdownHooks()
app.close() always runs the three shutdown hooks. Shutdown signals from outside, the way a deploy or a scale-down
stops a process, run them only when the app has called app.enableShutdownHooks(), because the listeners it adds cost
resources and the documentation leaves them off by default. The worker below prints each shutdown hook Nest calls on
it, and the program sends it SIGTERM: The signal a process manager sends to ask a process to stop, as a deploy or a scale-down does. Unlike SIGKILL it can be caught, so the process can clean up before it ends. four ways:
import { session } from '../session.mjs';
// A worker that prints each shutdown hook Nest calls, started four times: sent SIGTERM without and with
// enableShutdownHooks(), with useProcessExit, and closed by its own app.close() call.
session(`
npx tsc -p tsconfig.json
node dist/signals.js
`); Output
$ npx tsc -p tsconfig.json $ node dist/signals.js 1. SIGTERM, no enableShutdownHooks() ready => ended by SIGTERM, which a shell reports as exit status 143 2. SIGTERM after enableShutdownHooks() ready onModuleDestroy() beforeApplicationShutdown(SIGTERM) onApplicationShutdown(SIGTERM) => ended by SIGTERM, which a shell reports as exit status 143 3. the same, with useProcessExit: true ready onModuleDestroy() beforeApplicationShutdown(SIGTERM) onApplicationShutdown(SIGTERM) => exited with code 0 4. no signal: the program calls app.close() ready onModuleDestroy() beforeApplicationShutdown(undefined) onApplicationShutdown(undefined) => exited with code 0
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node signals.mjs
| Run | What started the shutdown | Hooks that ran, and their signal | How the process ended |
|---|---|---|---|
| 1 | SIGTERM, no enableShutdownHooks() |
none | by SIGTERM, status 143 |
| 2 | SIGTERM after enableShutdownHooks() |
all three, with SIGTERM |
by SIGTERM, status 143 |
| 3 | the same, with useProcessExit: true |
all three, with SIGTERM |
exit code 0 |
| 4 | the program’s own app.close() |
all three, with undefined |
exit code 0 |
Status 143 is 128 plus 15, the number of SIGTERM, which Node.js documents as the exit code of a process ended by a
signal. Node.js ends a process on SIGTERM unless a listener is installed. Nest’s shutdown hooks: Nest's listeners for process signals, switched on by app.enableShutdownHooks(): a signal such as SIGTERM then runs onModuleDestroy(), beforeApplicationShutdown() and onApplicationShutdown() before the process ends. are that listener: they keep the process alive, run the hooks, then remove the listener and send the
process the same signal again, so run 2 still ended as a SIGTERM exit. Run 4 ended only because nothing kept it alive
after the hooks: the documentation warns that app.close() does not end the process, and an open timer or socket keeps
it running. Two options of an HTTP app shape its last moments: return503OnClosing answers new requests with 503 once
the shutdown starts, and forceCloseConnections closes the open connections when the server closes.
Shutdown signals on Windows
On Windows, the documentation says, SIGINT works and SIGTERM never does, since the Task Manager stops a process unconditionally. Test the shutdown on the platform the app runs on.
Checkpoint
Check: Shutdown signals and enableShutdownHooks()
Draining a worker and an HTTP server on SIGTERM, without losing work Pro: opens with a Pro, Premium or Ultimate pass
A deploy should lose no job and no request. This part gives each of the three shutdown hooks one step of a clean stop, drains a kitchen worker and an HTTP server with them, and measures what each request sees.
In this part:
- A worker drained on SIGTERM, in the order of its modules
- What Nest does between SIGTERM and exit, step by step
- In-flight HTTP requests, measured: what finishes, what gets 503, what is refused
1 diagram · 3 runnable examples (JavaScript, TypeScript) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Hooks that hang, throw or pile up listeners, reproduced and fixed Pro: opens with a Pro, Premium or Ultimate pass
A shutdown hook can hang, throw or add one listener too many, and each of the three fails in its own way. This part reproduces a hang, an error in a hook and a listener warning in tests, and fixes each one.
In this part:
- A hook that never settles, reproduced and given a deadline
- A shutdown budget for a service with jobs, with numbers
- Errors in hooks, and listeners that pile up in tests
3 runnable examples (JavaScript, TypeScript) · about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: the order of the hooks
The exercise is the rule of the first section: given an app’s modules, return the order of the start-up and the shutdown hooks. Its sample tests include the lesson’s run:
Exercise · Medium · JavaScript
Predict the order of lifecycle hooks across modules
Write the rule Nest follows when it calls a lifecycle hook in every module, the one the lesson's first run printed.
hookOrder(modules, root) receives the modules of an app by name, each as { imports, global }, and the name of the root module. Return { init, shutdown }: the names of the modules in the order Nest calls onModuleInit() (and then, in the same order, onApplicationBootstrap()), and in the order it calls the three shutdown hooks.
- Only the modules the root reaches through
importsbelong to the app. - Nest registers the modules depth first: the root, then its first import and everything that import reaches, then its second import, and so on, each module the first time it is reached.
- Each module has a distance: 1 for the root, and one more than the module that imports it; a module reached by several paths takes the largest. A module with
global: truecounts as farther than any other. initlists the modules from the largest distance to the smallest; modules at the same distance keep their registration order.shutdownisinitreversed.
The inputs have no import cycles. Nest also orders the providers inside each module, which the tests leave out.
Starter code · hook-order.mjs
/**
* The order in which Nest calls a lifecycle hook module by module: the init order (onModuleInit, then the same order
* for onApplicationBootstrap) and the shutdown order (onModuleDestroy and the two others).
* modules: { [name]: { imports: [names], global?: true } }; root: the name of the root module.
*/
export function hookOrder(modules, root) {
const init = Object.keys(modules);
// Sort the modules by their distance from the root, the farthest first.
return { init, shutdown: init };
} The sample tests · hook-order.test.mjs
import { assert, test } from 'mysmartcopilot:test';
import { hookOrder } from './hook-order.mjs';
/** A module that imports these modules. */
const mod = (...imports) => ({ imports });
test('a chain of imports starts from its deepest module', () => {
const modules = { AppModule: mod('CacheModule'), CacheModule: mod('DbModule'), DbModule: mod() };
assert.deepEqual(hookOrder(modules, 'AppModule').init, ['DbModule', 'CacheModule', 'AppModule']);
});
test('shutdown runs in the reverse order', () => {
const modules = { AppModule: mod('CacheModule'), CacheModule: mod('DbModule'), DbModule: mod() };
assert.deepEqual(hookOrder(modules, 'AppModule').shutdown, ['AppModule', 'CacheModule', 'DbModule']);
});
test('a global module comes first, as in the lesson\'s run', () => {
const modules = {
AppModule: mod('ConfigModule', 'CacheModule'),
ConfigModule: { imports: [], global: true },
CacheModule: mod('DbModule'),
DbModule: mod(),
};
assert.deepEqual(hookOrder(modules, 'AppModule').init, ['ConfigModule', 'DbModule', 'CacheModule', 'AppModule']);
});
test('modules at the same distance keep the order Nest registered them in', () => {
const modules = { WorkerModule: mod('QueueModule', 'MetricsModule'), QueueModule: mod(), MetricsModule: mod() };
assert.deepEqual(hookOrder(modules, 'WorkerModule').init, ['QueueModule', 'MetricsModule', 'WorkerModule']);
});
test('a module reached by two paths takes the longer one', () => {
const modules = {
AppModule: mod('DbModule', 'OrdersModule'),
OrdersModule: mod('KitchensModule'),
KitchensModule: mod('DbModule'),
DbModule: mod(),
};
assert.deepEqual(hookOrder(modules, 'AppModule').init, ['DbModule', 'KitchensModule', 'OrdersModule', 'AppModule']);
});
test('registration is depth first: a deep module found early comes before a sibling found later', () => {
const modules = {
AppModule: mod('OrdersModule', 'MenusModule'),
OrdersModule: mod('KitchensModule'),
MenusModule: mod('PhotosModule'),
KitchensModule: mod(),
PhotosModule: mod(),
};
assert.deepEqual(hookOrder(modules, 'AppModule').init, ['KitchensModule', 'PhotosModule', 'OrdersModule', 'MenusModule', 'AppModule']);
});
test('a module the root never reaches is not part of the app', () => {
const modules = { AppModule: mod('DbModule'), DbModule: mod(), ReportsModule: mod('DbModule') };
assert.deepEqual(hookOrder(modules, 'AppModule'), { init: ['DbModule', 'AppModule'], shutdown: ['AppModule', 'DbModule'] });
}); A hint
Do it in three passes. First a recursive register(name) that pushes a name the first time it sees it and then calls itself on each import, which gives the registration order. Then a second walk from the root that carries the depth and keeps, for every module, the largest depth it was reached at. Last, sort a copy of the registration order by distance, largest first, treating global modules as Infinity: JavaScript's sort is stable, so modules at the same distance keep their order. Reverse that list for shutdown.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then answer the questions on the order of the hooks and on shutdown signals; the session opens with the idea your answers have shown to be the weakest:
Practice that adapts to you
Practise this lesson
8 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
-
Fresher to mid · Warm-up
In what order does Nest call onModuleInit() across the modules of an app, and how does the shutdown order relate to it?
Module by module, by distance from the root module along the imports: the farthest module first, global modules before any other, and the root module last. Nest awaits each module's hooks before it moves on, and calls onApplicationBootstrap() afterwards in the same order. Inside one module, Nest 12 calls a provider's hooks after those of the providers it depends on. The three shutdown hooks, onModuleDestroy(), beforeApplicationShutdown() and onApplicationShutdown(), run in the reverse order: the root first and the farthest module last, so a class shuts down before what it uses. In this lesson's run with a global ConfigModule, DbModule, CacheModule and AppModule, onModuleInit() ran for AppConfig, DbPool, CacheClient, CacheWarmer and OrdersService, and onModuleDestroy() ran for the same classes in reverse.
-
Mid to senior
During every deploy, TiffinTrail's kitchen worker loses the job it is processing: the job is marked started and never finished. The worker is a Nest application context that takes jobs from a queue. What is happening, and how would you change it so that a deploy loses nothing?
-
Mid to senior
Deploys of a Nest service on Kubernetes take about 30 seconds longer than they should, and the old Pods end with SIGKILL. The logs show "closing the broker connection" and nothing after it. What do you suspect, how do you confirm it, and what do you change?
-
Senior to staff
During rolling deploys, clients of a Nest API behind a load balancer see a few refused connections each time a Pod stops, although shutdown hooks are enabled and in-flight requests finish. Explain where the errors come from in Nest's shutdown sequence, and what you would change.
Key takeaways
- Nest calls
onModuleInit()andonApplicationBootstrap()from the farthest module to the root, global modules first, and the three shutdown hooks from the root to the farthest. - Inside one module, Nest 12 calls a provider’s hooks after the hooks of the providers it depends on.
app.close()always runs the shutdown hooks, with no signal; a signal such as SIGTERM runs them only afterapp.enableShutdownHooks().- With shutdown hooks, Nest runs the hooks and then ends the process with the same signal, exit status 143 for SIGTERM,
unless
useProcessExitmakes it exit with code 0.
Terms in this lesson
- lifecycle hook
- A method such as onModuleInit() or onApplicationShutdown() that Nest calls on providers, controllers and module classes at a fixed point of start-up or shutdown, and awaits when it returns a promise.
- shutdown hooks
- Nest's listeners for process signals, switched on by app.enableShutdownHooks(): a signal such as SIGTERM then runs onModuleDestroy(), beforeApplicationShutdown() and onApplicationShutdown() before the process ends.
- SIGTERM
- The signal a process manager sends to ask a process to stop, as a deploy or a scale-down does. Unlike SIGKILL it can be caught, so the process can clean up before it ends.
References
- NestJS documentation: Lifecycle events (NestJS)
- NestJS documentation: Migration guide (lifecycle hooks called by component hierarchy level) (NestJS)
- @nestjs/core source: nest-application-context.ts (the shutdown sequence and the signal listeners) (NestJS (GitHub))
- Node.js 24 documentation: process, signal events and exit codes (OpenJS Foundation)
- Node.js 24 documentation: events, emitter.setMaxListeners() (OpenJS Foundation)
- Node.js 24 documentation: http, server.close() and server.keepAliveTimeout (OpenJS Foundation)
- @nestjs/common source: interfaces/nest-application-options.interface.ts (return503OnClosing, forceCloseConnections) (NestJS (GitHub))
- Kubernetes documentation: Pod lifecycle, termination of Pods (The Kubernetes Authors)
Report a problem with this lesson
Kept only in this browser. Your Learn progress