Documentation/titan/Health
In this section

Health

A Titan application reports three independent signals:

SignalQuestion it answersFailure means
LivenessIs the process alive and responsive?Restart me
ReadinessShould the load balancer send me traffic right now?Take me out of rotation, leave me running
HealthyIs every dependency I rely on currently working?Investigate; possibly degraded service

The kernel exposes an aggregate roll-up directly: app.health() returns an IHealthStatus combining every registered module's health, and app.checkHealth(moduleName) returns one module's status. Richer indicator registration and HTTP probes come from the titan-health ecosystem module.

Reading health from inside the app

titan-health ships an HealthService and registers indicators with the application. Inject it where you need to read aggregate health:

import { Inject, Service } from '@omnitron-dev/titan';
import { Public } from '@omnitron-dev/titan/decorators';
import { HEALTH_SERVICE_TOKEN, type HealthService }
  from '@omnitron-dev/titan-health';

@Service({ name: 'Admin' })
class AdminService {
  constructor(@Inject(HEALTH_SERVICE_TOKEN) private readonly health: HealthService) {}

  @Public()
  async status() {
    return this.health.check();
  }
}

The exact shape of the return value and token names lives in the titan-health package — consult its source for the canonical API.

Indicators

An indicator answers "is this dependency healthy?". The aggregator collects indicators from registered sources and combines them with a worst-status-wins rule:

CompositionResult
All healthyhealthy
Any degraded, none unhealthydegraded
Any unhealthyunhealthy

degraded reports a warning; unhealthy reports a failed check. Readiness decides whether the application can serve a particular workload. An operational failure must not become a warning just because other checks pass, and a warning must remain visible when readiness permits traffic.

Adding an indicator

The indicator interface and registration helper live in titan-health. Typically:

import { Injectable } from '@omnitron-dev/titan';
import type { IHealthIndicator } from '@omnitron-dev/titan-health';

@Injectable()
export class StripeHealth implements IHealthIndicator {
  // Required. `registerIndicator` throws BadRequest without it —
  // it is also the key the indicator is reported under.
  readonly name = 'stripe';

  constructor(private readonly stripe: Stripe) {}

  async check() {
    try {
      await this.stripe.balance.retrieve();
      return { status: 'healthy' as const };
    } catch (e) {
      return {
        status:  'degraded' as const,
        details: { error: String(e) },
      };
    }
  }
}

Then register it. There are two paths, and they are not equivalent:

// 1. Module options — the module does `new IndicatorClass()` itself.
HealthModule.forRoot({ indicators: [ClockSkewHealth] });

// 2. Through DI, for an indicator that HAS dependencies.
@Inject(HEALTH_SERVICE_TOKEN) private readonly health: IHealthService;
// …then, once your own dependencies are resolved:
this.health.registerIndicator(this.stripeHealth);

The indicators array constructs with no arguments (new IndicatorClass()), so StripeHealth registered that way would get stripe === undefined and fail at its first check rather than at registration. Use it for indicators that need nothing; resolve the others from DI and hand over the instance.

Liveness vs readiness

The kernel separates these intentionally. Two scenarios make the distinction important:

  • Slow startup — your app is alive (liveness ✓) but still warming caches and connecting to dependencies (readiness ✗). The orchestrator should keep the process running but not send traffic yet.

  • Transient dependency failure — your app is alive (liveness ✓) and could serve cached responses (readiness ✓), but Stripe is down (healthy ✗). The orchestrator should keep traffic flowing; the monitoring system should fire an alert.

If you collapse these into a single signal, the orchestrator will restart your app (good thing → bad thing) when a dependency hiccups.

Data origin during development

Working-tree execution and an installed immutable release provide different origin evidence. A running process or a successful health probe does not establish that data came from a verified artifact. Report origin separately from database, transport and worker availability.

Make the application's readiness policy explicit. A workload that requires verified producers should reject missing origin evidence by default. A local development stack can explicitly allow operational readiness while retaining an origin warning. That choice must not change persisted provenance, relabel old records or admit unverified observations to consumers that require verified data. Do not infer this exception merely from a project name or a missing environment variable.

Preserve check severity through every adapter: a warning remains a warning in process health, CLI and Console; any actual failed dependency remains a failure. Document what an operator can use now, what evidence is missing, and what would make the warning clear. Test both policies through the actual configuration loader and the shared process-health adapter.

Probe routes

When titan-health is loaded with its HTTP probe support, it exposes routes that map to the three signals (typically /healthz, /readyz, plus a detailed JSON variant). The exact path names and status codes are configured per project — consult titan-health.

For Kubernetes (example):

livenessProbe:
  httpGet: { path: /healthz, port: 3000 }
  initialDelaySeconds: 5
  periodSeconds: 10

readinessProbe:
  httpGet: { path: /readyz, port: 3000 }
  initialDelaySeconds: 0
  periodSeconds: 3

Programmatic shutdown on unhealthy

const status = await app.health();   // aggregate IHealthStatus
if (status.status === 'unhealthy' && status.modules?.['database']?.status === 'unhealthy') {
  void app.stop();   // graceful stop; supervisor restarts the process
}

app.health() returns the aggregate IHealthStatus ({ status, message?, modules?, details? }); app.checkHealth(name) returns the status for a single module. Note HealthCheck ('health:check') is a reserved event the kernel does not emit on its own — drive health checks by calling app.health() (e.g. from a readiness probe or an interval), not by subscribing to it. This pattern is rare — usually you want the readiness probe to handle this transparently — but it is available when you need it.

Anti-patterns

  • Heavy work in indicators. Indicators are called frequently (every few seconds for readiness probes). They should be cheap pings, not full integration tests.

  • Treating every dependency as critical. Most dependencies should degrade, not fail. A cache being down does not mean you cannot serve requests; it means you serve them slower.

  • Ignoring liveness. A process that has deadlocked but kept responding to liveness will not be restarted. Liveness should exercise enough of the request path to detect a stuck event loop.