Skip to main content

Lifecycle reference

Knowing when during boot/shutdown each module runs which hook is the difference between a clean restart and a half-drained buffer. This page documents the lifecycle behaviour of every official module — verified against source — so you can predict where a console.log('foo') in your onStart will run.

The four phases

Recap of Application lifecycle:

PhaseFires when
onInitAfter construction, before any onStart fires
onStartOnce every onInit finished; in dependency order
onStopOn app.stop(); in reverse dependency order
onDestroyFinal cleanup phase after onStop

Modules are not obliged to implement all four — most use just one or two. The table below shows what each official module actually does.

Lifecycle behaviour by module

Official@omnitron-dev/titan-*

Maintained by the Omnitron team. Independent npm package.

titan-auth

HookWhat it does
onInit
onStart
onStop
onDestroy

titan-auth is lifecycle-passive. The JWT service has no long-lived resources to set up or tear down beyond the JWKS HTTP client it opens lazily on first use.

titan-cache

HookWhat it does
onInit
onStart
onStopL2 adapter (Redis) is released when its owning client closes
onDestroy

L1 (in-memory LRU/LFU) lives in JS heap. L2 (Redis) is owned by titan-redis and shuts down with that module.

titan-database

HookWhat it does
onInit
onStartConnection pool warmed up on first query (lazy)
onStopDrains and closes the connection pool
onDestroy

The onStop matters: a clean drain prevents in-flight queries from being killed mid-transaction.

titan-discovery

Implemented on both the module and the service.

HookModule (DiscoveryModule)Service (DiscoveryService)
onInitResolves Redis client; prepares scripts
onStartTriggers service startOpens pub/sub; registers this node; starts heartbeat
onStopTriggers service stopStops heartbeat; deregisters; closes pub/sub
onDestroyFinal cleanup

In clientMode: true, registration is skipped but pub/sub still opens on onStart so the node receives events.

titan-events

Each sub-service has its own onInit / onDestroy; the orchestrator EventsService adds onStart / onStop.

HookWhat happens
onInitHistory / metadata / scheduler / validation services initialise their stores
onStartBus subscribes; persisted scheduled jobs are re-armed
onStopDrain in-flight emissions; flush history
onDestroyRelease all listener references

titan-health

HookWhat it does
onInitRegisters configured indicators
onStart
onStop
onDestroy

Checks are pull-driven — Kubernetes probes invoke check() on demand. No background loop runs.

titan-lock

HookWhat it does
onInit
onStart
onStopBest-effort: releases locks currently held by this process
onDestroy

Releasing locks on onStop is best-effort because the process may already be in a degraded state. Always set a lock TTL so abandoned locks self-expire.

titan-metrics

Uses explicit start() / stop() methods rather than the lifecycle interface, called from module setup.

MethodWhat it does
start()Starts periodic collection (process, system, RPC); kicks off flush interval
stop()Drains the buffer to storage; stops collection; calls cleanup()

The module calls start() during initialisation and stop() during shutdown — you don't have to wire it up by hand.

titan-notifications

HookWhat it does
onInitBuilds channel registry; resolves transports
onStartProducer connects to messaging transport; worker subscribes to queues
onStopStop consuming new messages; drain in-flight delivery
onDestroyDisconnect transport; release channel resources

forWorker(...) registrations subscribe to the messaging transport on onStart — the producer (registered via forRoot) opens its publisher connection in the same phase.

titan-pm

HookWhat it does
onInitInitialise registry / spawner / health checker / metrics
onStartStart any auto-start processes from config
onStopSend SIGTERM to every child; wait for the configured grace period; SIGKILL survivors
onDestroyFinal reap of any stragglers

Crash-supervised children stop in the opposite order they started — gives you a chance to drain a worker before its dependencies disappear.

titan-ratelimit

Uses an explicit destroy() method called during module shutdown.

MethodWhat it does
destroy()Stops the periodic cleanup timer on in-memory storage; closes Redis storage client

titan-redis

HookWhat it does
onModuleInitEstablishes the connection(s) configured via forRoot / forFeature
onStopDisconnects all named clients gracefully

onModuleInit is the legacy name in titan-redis — functionally equivalent to onInit in newer modules.

titan-scheduler

HookWhat it does
onInitResolve persistence backend; rebuild registry from store
onStartSchedule all registered jobs; arm cron timers / intervals
onStopCancel pending timers; flush in-flight to persistence
onDestroyRelease listeners; close persistence backend

The split matters: onInit rebuilds the registry, onStart actually arms the timers. Don't expect jobs to fire between those two.

titan-telemetry-relay

Not a module — instantiated directly. Lifecycle is manual:

MethodWhat it does
start()Open WAL; start flush interval; start aggregator subscriptions
stop()Stop flush; drain WAL; close aggregator

When integrated into an app via DI, wrap it in a thin lifecycle adapter:

@Injectable()
class TelemetryRelayAdapter implements OnStart, OnStop {
constructor(private readonly relay: TelemetryRelayService) {}
async onStart() { await this.relay.start(); }
async onStop() { await this.relay.stop(); }
}

Built-in modules

Built-in@omnitron-dev/titanconfig + logger

Ships inside @omnitron-dev/titan. No additional install required.

config

HookWhat it does
onInitLoad all sources in order; deep-merge; validate against schema
onStartIf watchForChanges: true, start file watchers
onStopStop file watchers
onDestroyRelease schema reference

If validation fails in onInit, the app aborts boot with the underlying Zod error — no partial start.

logger

HookWhat it does
onInitOpen transports (file, network, console)
onStart
onStopFlush all transports before draining (critical: lossy without this)
onDestroyClose transport handles

The flush in onStop is why graceful shutdown matters for observability — SIGKILL skips it and you lose the last few seconds of logs.

Putting it together — full boot/shutdown timeline

Practical rules

  • Long-running background work belongs in onStart, not onInit. Hooks at onInit should be fast — they block every later module from initialising.
  • Resources opened in onInit should close in onDestroy. Resources started in onStart should stop in onStop. Mixing them causes subtle leaks.
  • onStop runs in reverse dependency order. Your services drain before the modules they depend on. Don't rely on Redis inside an onStop if your service's onStop was already followed by Redis disconnecting.
  • Don't throw in onStop / onDestroy. A throw aborts the remaining cleanup chain. Catch + log, let the process exit.
  • Heavy CPU work in onStart blocks readiness. If you must precompute, do it in a background task started from onStart and gate readiness on a separate flag.

See also