Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Node.js 24.21.0, NestJS 12.1.2, TypeScript 6.0.3 and quickjs 0.32.0
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: Five hooks run in a fixed order, module by module · Shutdown signals run the hooks only after enableShutdownHooks(). With a pass: 2 in-depth parts and 3 interview answers, about 13 minutes.

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 every onModuleDestroy() 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:

Four modules, five hooks: the order of the calls JavaScript · lifecycle/hook-order.mjs
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:

Interactive · step by step The order of the hooks across four modules Step through start-up and app.close() of the run. The module or provider in focus is the one whose hook Nest calls.
The order of the hooks across four modules, step 7 of 7. Showing AppModule, ConfigModule, CacheModule, CacheModule's providers (CacheWarmer's hook first), DbModule. In focus: ConfigModule.importsimportsimportsholdsAppModuledistance 1: the rootConfigModuleglobal: counted farthestCacheModuledistance 2CacheModule's providersCacheWarmer's hook firstDbModuledistance 3shutdown ends here

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. Then the deepest import. DbModule, at distance 3, comes next. DbPool's hook ran before any class that depends on it.
  4. 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.
  5. 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.
  6. 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.
  7. 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

  1. Question 1 of 2 AppModule imports ConfigModule, which is global, and CacheModule; CacheModule imports DbModule. Whose onModuleInit() does Nest call first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: AppConfig's, in the global ConfigModule

    Start-up hooks run module by module from the farthest to the root: ConfigModule (global), DbModule (3), CacheModule (2), AppModule (1).

  2. Question 2 of 2 In the same app, app.close() is called. Whose onModuleDestroy() runs first, and in CacheModule, which of CacheClient and CacheWarmer (which needs CacheClient) goes first?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: OrdersService's first, and in CacheModule CacheWarmer before CacheClient

    The run printed onModuleDestroy for OrdersService, CacheWarmer, CacheClient, DbPool and AppConfig, in that order.

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:

A worker sent SIGTERM, with and without shutdown hooks JavaScript · lifecycle/signals.mjs
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()

  1. Question 1 of 2 A Nest app that never called enableShutdownHooks() receives SIGTERM. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The process ends at once, by SIGTERM, and no shutdown hook runs

    Off by default: app.close() runs the hooks, a signal does not, until the app enables shutdown hooks.

  2. Question 2 of 2 The program calls app.close() itself. What do beforeApplicationShutdown() and onApplicationShutdown() receive as their signal?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: undefined

    A hook that decides what to do from its signal must handle undefined, the value of every shutdown started by app.close().

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.

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 imports belong 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: true counts as farther than any other.
  • init lists the modules from the largest distance to the smallest; modules at the same distance keep their registration order. shutdown is init reversed.

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.

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.

  1. Question 1 of 8 When does onApplicationBootstrap() run?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: Once every module has been initialized, before a server starts listening

    onModuleInit() runs module by module; when every module is done, onApplicationBootstrap() runs over them in the same order.

  2. Question 2 of 8 OrdersModule imports KitchensModule, which imports DbModule; AppModule imports OrdersModule and DbModule. In which module order does onModuleInit() run?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: DbModule, KitchensModule, OrdersModule, AppModule

    A module reached by two paths takes the longer one, which is the rule of the exercise's test with the same app.

  3. Question 3 of 8 Which classes does Nest never call lifecycle hooks on?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: Request-scoped classes

    A request-scoped instance lives for one request, so it has no place in start-up or shutdown.

  4. Question 4 of 8 After an upgrade from Nest 11 to 12, a module's MetricsReporter, listed first in providers and injecting MetricsClient, now runs its onModuleInit() after MetricsClient's. Why?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: Nest 12 calls hooks by component hierarchy level, so a provider's hooks follow those of the providers it depends on

    Code that relied on the providers list for hook order should depend on what it needs instead, which Nest 12 now honours.

  5. Question 5 of 8 A Node.js process is ended by SIGTERM, signal number 15. Which exit status does a shell report?

    Type a number.

    Show the answer: Question 5 of 8

    Answer: 143

    128 plus the signal's number, 128 + 15 = 143, which runs 1 and 2 reported.

  6. Question 6 of 8 Why does Nest leave shutdown hooks off until enableShutdownHooks() is called?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: The signal listeners it adds cost resources, so the documentation leaves them to the app

    Enable them once, in main.ts, where the process that receives the signal starts.

  7. Question 7 of 8 A script calls await app.close() at the end, yet the process keeps running. Which explanation fits?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: Something still keeps the event loop busy, such as an open timer or socket; app.close() does not end the process

    Close timers, sockets and pools in the shutdown hooks; a process with nothing left to do then ends by itself.

  8. Question 8 of 8 After every deploy, a dashboard counts each old process of a Nest service as failed, because it ended with exit status 143, although all three shutdown hooks ran. What does 143 say here, and which option makes these processes end with 0?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: 143 is 128 + 15, an end by SIGTERM, which Nest sends the process again after the hooks; useProcessExit true makes it exit with code 0 instead

    With shutdown hooks, Nest runs the hooks, removes its listener and sends the process the same signal again, so a clean shutdown still reads 143. useProcessExit trades that for exit code 0; either way, tell the dashboard which status a clean stop gives.

Interview questions

  1. 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.

  2. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

  4. 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.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Nest calls onModuleInit() and onApplicationBootstrap() 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 after app.enableShutdownHooks().
  • With shutdown hooks, Nest runs the hooks and then ends the process with the same signal, exit status 143 for SIGTERM, unless useProcessExit makes 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.