Lifecycle Events & Shutdown

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against NestJS 12.1 (npm 12.1.2), TypeScript 6 (6.0.3), Node.js 20.19+ or 22.12+, Express 5 and Fastify 5, and is current as of 2026-10-02. The examples use a project generated by the Nest 12 CLI (ESM layout, Vitest, supertest). The console output shown for the shutdown examples is what a real Node.js process prints when it receives a real SIGTERM.

A Nest application has a lifecycle that Nest manages: it resolves providers, initialises modules, starts listening, and later stops listening and tears everything down. Nest exposes this lifecycle through lifecycle hooks: methods that you implement on a module, provider or controller and that Nest calls at a fixed moment. They are where you open and close database pools, warm caches, register with a broker and release resources. They are also the part of Nest that makes a deployment on an orchestrator such as Kubernetes safe, because shutdown is where in-flight work either finishes or gets lost.

The lifecycle at a glance

The official documentation divides the lifecycle into three phases: initializing, running and terminating. The following timeline shows what happens in each phase and where the hooks fire.

flowchart LR subgraph B["1. Bootstrap"] direction TB b1["NestFactory.create() resolves the module graph"] --> b2["onModuleInit per module, awaited"] b2 --> b3["onApplicationBootstrap per module, awaited"] end subgraph S["2. Serving"] direction TB s1["app.listen() starts the HTTP server"] --> s2["Requests flow through middleware, guards, pipes, interceptors, handlers"] end subgraph D["3. Shutdown"] direction TB d1["SIGTERM or SIGINT, or app.close()"] --> d2["onModuleDestroy"] d2 --> d3["beforeApplicationShutdown"] d3 --> d4["Server stops accepting connections and drains"] d4 --> d5["onApplicationShutdown"] d5 --> d6["Process exits"] end B --> S --> D

The five lifecycle hooks

Each hook is represented by an interface (OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown) with one method of the same name in camelCase. The interfaces are technically optional because they disappear at compile time, but implementing them gives you type checking and editor help, so it is good practice to use them.

Hook Called when Notes

onModuleInit()

Once the host module’s dependencies have been resolved.

Runs only on initialisation (app.init(), app.listen() or NestFactory.createApplicationContext()).

onApplicationBootstrap()

Once all modules have been initialised, but before the application listens for connections.

Runs only on initialisation. Safe place to use state that other modules set up in onModuleInit().

onModuleDestroy()

After a termination signal such as SIGTERM has been received, or when app.close() is called.

Takes no arguments.

beforeApplicationShutdown(signal?)

After every onModuleDestroy() has completed (promises resolved or rejected). When it completes, existing connections are closed.

Receives the signal as its first argument when the shutdown was caused by one.

onApplicationShutdown(signal?)

After connections have closed, as the last step of app.close().

Receives the signal as its first argument when the shutdown was caused by one.

The three shutdown hooks run only when you call app.close() explicitly, or when the process receives a system signal and you called enableShutdownHooks() (see below). The two start-up hooks run on initialisation and work without any opt-in.

The hooks are not triggered for request-scoped classes. A request-scoped instance is created for each request and garbage-collected after the response, so it is not tied to the application lifecycle and its lifespan is unpredictable.

The class below implements all five hooks through a small base class that logs each call, so you can see exactly when they fire. Real services override only the hooks they need.

traced-hooks.ts
import {
  BeforeApplicationShutdown,
  OnApplicationBootstrap,
  OnApplicationShutdown,
  OnModuleDestroy,
  OnModuleInit,
} from '@nestjs/common';
import { record } from './trace.js'; // pushes the entry to an array and logs it

export abstract class TracedHooks
  implements
    OnModuleInit,
    OnApplicationBootstrap,
    OnModuleDestroy,
    BeforeApplicationShutdown,
    OnApplicationShutdown
{
  protected abstract readonly label: string;

  onModuleInit(): void {
    record(`${this.label}.onModuleInit`);
  }

  onApplicationBootstrap(): void {
    record(`${this.label}.onApplicationBootstrap`);
  }

  onModuleDestroy(): void {
    record(`${this.label}.onModuleDestroy`);
  }

  beforeApplicationShutdown(signal?: string): void {
    record(`${this.label}.beforeApplicationShutdown(${signal ?? 'no signal'})`);
  }

  onApplicationShutdown(signal?: string): void {
    record(`${this.label}.onApplicationShutdown(${signal ?? 'no signal'})`);
  }
}

Hooks can live on modules as well as on providers and controllers. A module class that implements a hook is run like any other component, which is handy for module-level set-up such as registering something with a module’s own providers. The modules and providers pages cover the building blocks the hooks attach to.

Ordering: v11 and v12 rules

The order matters whenever one component depends on another being ready, or must be torn down before it. Three rules apply on Nest 12.

  • Initialisation follows the import graph. Nest calls onModuleInit() and onApplicationBootstrap() module by module, ordered by each module’s distance from the root module: the most deeply imported modules (and global modules, which count as a dependency of every module) go first and the root module goes last. Each module’s hooks are awaited before Nest moves on.

  • Shutdown runs in the reverse order. Since Nest 11 the termination hooks (onModuleDestroy(), beforeApplicationShutdown(), onApplicationShutdown()) are executed in the reverse order to their initialisation counterparts. In a chain A → B → C, initialisation goes C, B, A, and destruction goes A, B, C. Before Nest 11 the destruction order was not guaranteed to be the reverse; the 11.0.0 release notes list the fixes ("Order of module destroy should be the reverse of module init" and "fix shutdown hooks order").

  • Hooks are called by component hierarchy level (v12). In Nest 12 the hook calls are organised by hierarchy level, which can change the order between related providers or modules compared with Nest 11. The migration guide advises reviewing any initialisation logic, teardown logic or tests that rely on a specific hook order between related providers.

The following example makes the order visible. A RootModule imports CatalogModule, which imports CoreModule. CatalogService injects CoreService. Every class extends the TracedHooks base class above.

ordering.module.ts
import { Injectable, Module } from '@nestjs/common';
import { TracedHooks } from './traced-hooks.js';

@Injectable()
export class CoreService extends TracedHooks {
  protected readonly label = 'CoreService';
}

@Module({ providers: [CoreService], exports: [CoreService] })
export class CoreModule extends TracedHooks {
  protected readonly label = 'CoreModule';
}

@Injectable()
export class CatalogService extends TracedHooks {
  protected readonly label = 'CatalogService';
  constructor(readonly core: CoreService) {
    super();
  }
}

@Module({ imports: [CoreModule], providers: [CatalogService] })
export class CatalogModule extends TracedHooks {
  protected readonly label = 'CatalogModule';
}

@Module({ imports: [CatalogModule] })
export class RootModule extends TracedHooks {
  protected readonly label = 'RootModule';
}

Initialising and then closing an application built from RootModule (app.init() followed by app.close()) prints this on NestJS 12.1.2:

Start-up output
CoreService.onModuleInit
CoreModule.onModuleInit
CatalogService.onModuleInit
CatalogModule.onModuleInit
RootModule.onModuleInit
CoreService.onApplicationBootstrap
CoreModule.onApplicationBootstrap
CatalogService.onApplicationBootstrap
CatalogModule.onApplicationBootstrap
RootModule.onApplicationBootstrap
Shutdown output
RootModule.onModuleDestroy
CatalogService.onModuleDestroy
CatalogModule.onModuleDestroy
CoreService.onModuleDestroy
CoreModule.onModuleDestroy
RootModule.beforeApplicationShutdown(no signal)
CatalogService.beforeApplicationShutdown(no signal)
CatalogModule.beforeApplicationShutdown(no signal)
CoreService.beforeApplicationShutdown(no signal)
CoreModule.beforeApplicationShutdown(no signal)
RootModule.onApplicationShutdown(no signal)
CatalogService.onApplicationShutdown(no signal)
CatalogModule.onApplicationShutdown(no signal)
CoreService.onApplicationShutdown(no signal)
CoreModule.onApplicationShutdown(no signal)

Read the output as follows. The deepest module (CoreModule) initialises first and the root last; all onModuleInit() calls complete before any onApplicationBootstrap() call starts. On shutdown the root goes first and the deepest module last. Inside one module the output shows the module’s providers and the module class interleaved, so treat the order between components of the same module as an implementation detail and do not rely on it. Rely on the module-level rule. If two providers in one module need a strict relative order, make the dependency explicit by injecting one into the other, or move them into separate modules so the import graph defines the order.

Asynchronous initialisation

onModuleInit() and onApplicationBootstrap() can defer start-up. Return a Promise, or mark the method async and await inside it: Nest waits for the promise before moving to the next step, so app.listen() does not start accepting connections until every hook has finished. This is the right place to connect to a database, run a migration check or warm a cache.

async-init.service.ts
import { Injectable, Logger, OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
import { setTimeout as sleep } from 'node:timers/promises';

@Injectable()
export class SearchIndexService implements OnModuleInit, OnApplicationBootstrap {
  private readonly logger = new Logger(SearchIndexService.name);
  private ready = false;

  // Nest awaits this promise before it moves on: nothing listens until it resolves.
  async onModuleInit(): Promise<void> {
    await sleep(100); // stands in for connecting to a database or warming a cache
    this.ready = true;
  }

  onApplicationBootstrap(): void {
    // Every module has finished onModuleInit by now, so cross-module state is safe to use.
    this.logger.log(`index ready: ${this.ready}`);
  }

  get isReady(): boolean {
    return this.ready;
  }
}

The application prints, in order, onModuleInit:start, onModuleInit:done and then onApplicationBootstrap(ready=true), and isReady is true as soon as app.init() resolves. Two consequences follow.

  • A slow hook delays the start-up of the whole application, including readiness on an orchestrator. Keep hooks fast, or give them a timeout.

  • A hook that throws or returns a rejected promise makes app.init() or app.listen() reject, so the application fails to start. That is usually what you want for a mandatory dependency such as the database, and the reason a failed hook should not be swallowed.

Providers that must exist before they can be injected, such as a connection, are better created with an async factory provider (useFactory returning a promise), which Nest also awaits during module resolution; use hooks for work that happens after the provider exists. Async factory providers are covered on the providers page.

Enabling shutdown hooks and handling signals

By default the three shutdown hooks do not run on a process signal, because the signal listeners consume resources. If you do nothing, SIGTERM or SIGINT terminates the process immediately and no hook runs. To opt in, call enableShutdownHooks() at bootstrap:

main.ts
import { NestFactory } from '@nestjs/core';
import { ShutdownDemoModule } from './shutdown-demo.module.js';

const app = await NestFactory.create(ShutdownDemoModule);

// Without this call SIGTERM/SIGINT kill the process and no shutdown hook runs.
app.enableShutdownHooks();

await app.listen(Number(process.env.PORT ?? 3000));

When a signal arrives, Nest runs onModuleDestroy(), beforeApplicationShutdown() and onApplicationShutdown() in that order, waiting for any promise a hook returns. beforeApplicationShutdown() and onApplicationShutdown() receive the signal name (for example "SIGTERM") as their first parameter; onModuleDestroy() takes no arguments. The ConnectionPool below uses the three hooks the way a real pool would: stop taking new work, drain what is queued, then close.

shutdown-demo.module.ts (excerpt)
@Injectable()
export class ConnectionPool
  implements OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown
{
  onModuleDestroy(): void {
    console.log('pool: stop accepting new work');
  }

  async beforeApplicationShutdown(signal?: string): Promise<void> {
    console.log(`pool: draining queued work (signal=${signal})`);
    await sleep(100);
  }

  onApplicationShutdown(signal?: string): void {
    console.log(`pool: closed (signal=${signal})`);
  }
}

Running the compiled application and sending it SIGTERM prints:

pool: stop accepting new work
pool: draining queued work (signal=SIGTERM)
pool: closed (signal=SIGTERM)

Without the enableShutdownHooks() call, the same process sent SIGTERM prints nothing and exits immediately with the signal.

Some practical points about signals:

  • app.close() runs the shutdown sequence (the three hooks and closing the server) but does not terminate the Node.js process. If an interval, a timer, a client connection or a background task is still active, the process stays alive. Release those resources in your hooks. With enableShutdownHooks(), after the sequence finishes Nest re-raises the signal so the process ends with it (the real process above exited with signal SIGTERM).

  • You can call app.close() yourself, for example in tests or after a fatal error. In that case the hooks receive no signal.

  • enableShutdownHooks() starts listeners, so calling it in many applications within one Node.js process, as parallel test suites do, can trigger Node.js’s "too many listeners" warning. Call it in main.ts, not in test set-up.

  • On Windows the support is limited. SIGINT works, SIGBREAK works, and SIGHUP works to some extent, but SIGTERM never does, because ending a process from the Task Manager is unconditional. Linux containers, where Kubernetes sends SIGTERM, are not affected.

  • A process manager or a shell wrapper between the orchestrator and node may not forward the signal. Start Node directly (node dist/main.js) so it is the process that receives SIGTERM.

Graceful shutdown

Graceful shutdown means the application stops accepting new requests, lets the requests already running finish, releases its resources, and then exits. In Nest the pieces are the hooks above plus the HTTP adapter’s behaviour when the server closes.

In-flight requests: what the Express adapter does

By default the HTTP adapters wait for responses to finish before the application closes. On Express 5 (@nestjs/platform-express) NestJS 12 added an explicit drain on shutdown: the migration guide lists "Express graceful shutdown: the Express adapter now drains in-flight requests on shutdown". The /slow route of the demo application takes 1.5 seconds. A client sent a request, and 300 ms later the process received SIGTERM. The client still received its full response:

{"done":true}

and the process exited right after (about 1.2 seconds after the signal, which is the time the request still needed), with the hooks running after the response was delivered. Requests that arrive after the server has closed are refused, and a load balancer should already have stopped sending them (see the Kubernetes section).

Keep-alive connections and forceCloseConnections

A browser or an HTTP client that uses Connection: keep-alive keeps its socket open after the response. The server can only finish closing when those sockets are gone, so a shutdown can be held up by an idle keep-alive connection. In the demo, the same in-flight request made by a client that uses a keep-alive agent was answered after about 1.2 seconds, but the process took about 7 seconds to exit, because it was waiting for the idle connection to expire. The official FAQ describes this symptom: an application that does not exit when you expect it to, most often with enableShutdownHooks() enabled and most often during development with --watch.

The forceCloseConnections option makes the application destroy open connections instead of waiting for them:

main.ts (Express)
import { NestFactory } from '@nestjs/core';
import { ShutdownDemoModule } from './shutdown-demo.module.js';

const app = await NestFactory.create(ShutdownDemoModule, {
  // Express adapter: destroy keep-alive sockets instead of waiting for them.
  forceCloseConnections: true,
});
app.enableShutdownHooks();
await app.listen(3000);

On Fastify the option belongs to Fastify’s own server options, so you pass it to the adapter constructor:

main-fastify.ts
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { ShutdownDemoModule } from './shutdown-demo.module.js';

const app = await NestFactory.create<NestFastifyApplication>(
  ShutdownDemoModule,
  // Fastify takes the option on the adapter instead of the application options.
  new FastifyAdapter({ forceCloseConnections: true }),
);
app.enableShutdownHooks();
await app.listen({ port: Number(process.env.PORT ?? 3000), host: '0.0.0.0' });

The trade-off is that the option does not distinguish idle sockets from busy ones: with it enabled on Express, a client whose request is still running when SIGTERM arrives sees its connection reset and the process exits about 0.1 seconds after the signal, and Fastify with forceCloseConnections: true behaves the same way. In other words forceCloseConnections is a fast exit, not a graceful one. Most applications do not need it; the official page says so. A reasonable policy is to use it for development with --watch, and in production to prefer a bounded wait, a short keep-alive timeout, or a proxy that closes idle connections, so that in-flight requests are not cut.

Graceful shutdown for WebSockets and microservices

The same app.close() also closes the WebSocket gateways and any connected microservice transports, after the hooks have run. If your transport holds in-flight messages, finish or acknowledge them in beforeApplicationShutdown(), which runs while connections are still open, and do the final clean-up in onApplicationShutdown(), which runs after they have closed.

Kubernetes termination

Kubernetes stops a container by sending SIGTERM, waiting up to terminationGracePeriodSeconds, and then sending SIGKILL. At the same time it removes the pod from the endpoints behind the Service. Those two actions are not synchronised: for a short time after the signal, traffic can still arrive. A Nest application therefore needs three things:

  1. enableShutdownHooks(), so that SIGTERM starts the graceful sequence instead of killing the process.

  2. A readiness probe that starts failing as soon as shutdown begins, so Kubernetes stops routing traffic to the pod.

  3. A delay between the signal and the closing of the server that is slightly longer than the readiness-probe interval, so the probe has time to fail before the server stops listening.

The Terminus health-check module covers the second and third point. When its health endpoint is used as a readiness probe, it reports "shutting_down" with status 503 as soon as a termination signal arrives, and the gracefulShutdownTimeoutMs option (in milliseconds, 0 by default) delays the shutdown while the application keeps finishing in-flight requests:

Terminus with a shutdown delay
@Module({
  imports: [
    TerminusModule.forRoot({
      // Keep answering (with 503 "shutting_down") for a moment after SIGTERM
      // so the load balancer notices before the server stops listening.
      gracefulShutdownTimeoutMs: Number(process.env.GRACE_MS ?? 0),
    }),
  ],
  controllers: [DemoController],
  providers: [ConnectionPool],
})
export class ShutdownDemoModule {}

The DemoController exposes GET /health using HealthCheckService and the @HealthCheck() decorator. With GRACE_MS=1000, a request made 300 ms after SIGTERM returned:

503 {"status":"shutting_down","info":{},"error":{},"details":{}}

The log shows Received termination signal SIGTERM, Awaiting 1000ms before shutdown and Timeout reached, shutting down now. The process exits about 1.1 seconds after the signal, and pool: closed is printed after the delay, so the hooks run only once the grace period has elapsed. Note that the delay is applied on SIGTERM, the signal orchestrators send. Other signals such as SIGINT (Ctrl+C) switch the health check to shutting_down too, but shut the application down immediately. Health checks themselves, and the readiness and liveness endpoints, are part of the observability and reliability pages of this section.

The Kubernetes side is a Deployment whose readiness probe hits that endpoint more often than the delay, and whose terminationGracePeriodSeconds is longer than the delay plus the time your in-flight requests and shutdown hooks need:

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bookshelf-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: bookshelf-api
  template:
    metadata:
      labels:
        app: bookshelf-api
    spec:
      # Must be longer than GRACE_MS plus the time in-flight requests and the
      # shutdown hooks need. After this the kubelet sends SIGKILL.
      terminationGracePeriodSeconds: 30
      containers:
        - name: api
          image: ghcr.io/example/bookshelf-api:1.0.0
          # Run node directly so it receives SIGTERM as PID 1.
          command: ["node", "dist/main.js"]
          env:
            - name: GRACE_MS
              value: "5000" # longer than the readiness period below
          ports:
            - containerPort: 3000
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            periodSeconds: 2
            failureThreshold: 1

The manifest was parsed as YAML to check its syntax; it was not applied to a cluster, so treat the numbers as a starting point to tune. The timing relationship is what matters: probe period (2 s) < GRACE_MS (5 s) < terminationGracePeriodSeconds (30 s). If the delay were longer than the grace period, Kubernetes would kill the process with SIGKILL before the hooks run, and the hooks would never finish.

Common pitfalls

  • Forgetting enableShutdownHooks(). The three shutdown hooks silently never run on SIGTERM; connections are cut and queued work is lost.

  • Expecting hooks on request-scoped providers. They are never called; clean up per-request resources in the request’s own flow.

  • Doing heavy work in onModuleInit(). It delays every start-up and every readiness check. Move slow, non-essential work to onApplicationBootstrap() or to a background job.

  • Relying on the order between providers of one module. The documented rule is per module; make dependencies explicit.

  • Running through a wrapper. A shell or package-manager wrapper may not forward SIGTERM to Node.

  • A grace period that is too short. terminationGracePeriodSeconds must cover the shutdown delay, the longest in-flight request you accept, and the hooks.

  • Using forceCloseConnections in production and calling it graceful. It destroys in-flight requests too.

What changed recently

  • Nest 11 made the termination hooks run in the reverse order of initialisation, with global modules initialised first and destroyed last.

  • Nest 12 calls hooks by component hierarchy level, which can change the order between related providers and modules, and the Express adapter drains in-flight requests on shutdown. Both are listed in the migration guide.

  • All first-party Nest 12 packages are ESM-only. The hook interfaces and enableShutdownHooks() are unchanged, and the examples use the .js import extensions the ESM template needs.

  • Terminus was updated for Nest 12 (the legacy HealthIndicator and HealthCheckError classes were removed). The gracefulShutdownTimeoutMs option shown above is unchanged.

Where the book is outdated

The reference book, published in January 2025 (see the section bibliography), does not cover lifecycle events or enableShutdownHooks(), and it predates the Nest 11 and Nest 12 changes described above (reverse-order termination hooks, hooks by hierarchy level, the Express drain). For graceful shutdown use the combination shown on this page rather than calling process.exit() from a signal handler, which skips the hooks.