NestJS health check endpoint with Terminus

Install @nestjs/terminus, inject HealthCheckService and TypeOrmHealthIndicator into a /health controller, and the endpoint returns 200 while every indicator is up and 503 the moment one goes down.

A health endpoint answers one question: can this instance serve a real request right now. A controller returning a hardcoded ok answers a different one, which is whether the Node process is still accepting sockets, and sockets are almost never what break. The database is. Postgres restarts, the pool never recovers, every real route starts throwing 500s, and the health route, which touches nothing, keeps answering 200 in three milliseconds. A monitor reads the status code and nothing else, so it stays green for the whole outage.

Logdash flips a monitor to down on any status code outside 200-399 and fires the alert on that transition. That is the whole contract, and it puts the burden on your endpoint. A correct 503 is what turns the route into an alert. A 200 from a process that cannot reach its database turns your monitoring into decoration you trust a little less every month.

The Terminus controller

The @nestjs/terminus package is the idiomatic answer, and it earns its place because the failure mapping is already right. HealthCheckService.check runs your indicators, returns the aggregate when they pass, and throws ServiceUnavailableException the moment one reports down, which Nest serialises as a 503 naming the failing indicator.

health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import {
  HealthCheck,
  HealthCheckService,
  TypeOrmHealthIndicator,
} from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  constructor(
    private readonly health: HealthCheckService,
    private readonly db: TypeOrmHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([() => this.db.pingCheck('database')]);
  }
}

Import TerminusModule into the module that declares the controller. pingCheck runs a driver-level ping through the TypeORM connection, so it costs about what SELECT 1 costs and fails when the pool is exhausted or the host is unreachable. Swap in PrismaHealthIndicator, MongooseHealthIndicator, SequelizeHealthIndicator or MikroOrmHealthIndicator depending on what you run.

Without the package

If you would rather not add a dependency, the hand-rolled version is fifteen lines. Throw ServiceUnavailableException rather than reaching for the raw response object, so Nest keeps serialisation and your exception filters still apply.

health/health.controller.ts, without Terminus
import { Controller, Get, ServiceUnavailableException } from '@nestjs/common';
import { DataSource } from 'typeorm';

@Controller('health')
export class HealthController {
  constructor(private readonly dataSource: DataSource) {}

  @Get()
  async check() {
    try {
      await this.dataSource.query('SELECT 1');
    } catch {
      // 503, so a live process with a dead pool reads as down
      throw new ServiceUnavailableException({ status: 'down', db: 'unreachable' });
    }

    return { status: 'ok' };
  }
}

What to check and what to leave out

  • The database, with one cheap query. A ping through the pool you already hold proves the pool is alive, which is the failure you are trying to catch.
  • Redis or the queue only if a request that cannot reach them fails. A cache miss that degrades to a slower response is not a reason to pull the instance out of rotation.
  • Nothing that calls a third-party API. A payment provider having a bad afternoon should not take your monitor down with it.
  • Keep the handler under a second. The Logdash pinger gives up after 10 seconds and records the check as down, and anything close to that is already too slow to trust.
  • No connection strings, no environment names, no build SHA in the body. The endpoint is public unless you guard it, and Terminus prints whatever keys you hand it.

Point a monitor at it

  1. 1
    Prove the 503 by hand Run curl -i https://yourapp.com/health and read the status line, then stop the database and run it again. If the second call is not a 503, the monitor you are about to create cannot help you.
  2. 2
    Create the monitor Add an HTTP monitor on that URL. Logdash records the status code and the response time on every check: every 5 minutes on the free plan, every minute on Builder, every 15 seconds on Pro.
  3. 3
    Break it on purpose Stop the database again and wait one interval. The monitor flips to down and a Telegram alert arrives naming the endpoint and the status code, which is how you learn your alerting works before an outage does.

Degraded still returns 200

Terminus has three indicator outcomes, not two. An indicator can report up, degraded or down, and only down produces the 503. Degraded leaves the HTTP status at 200, so no external monitor will ever see it. If a degraded dependency means you cannot serve traffic, report it down and take the alert. Terminus also answers 503 while the app shuts down, so a check landing mid-deploy registers as a short blip.

What path should the NestJS health check endpoint use?

Use /health. It is what Kubernetes examples, load balancer defaults and most monitoring tools assume, and a Nest controller decorated with @Controller("health") gives you exactly that with no route prefix work.

How do I add a Redis health check in NestJS?

Inject MicroserviceHealthIndicator and call pingCheck<RedisOptions>("redis", { transport: Transport.REDIS, options: { host, port } }). It opens a client, connects and closes it, with a 1 second default timeout. Only add it if a request that cannot reach Redis actually fails.

How do I health check a gRPC service in NestJS?

GRPCHealthIndicator.checkService<GrpcOptions>("hero_service", "hero.health.v1") speaks the standard grpc.health.v1 protocol, so it works against any server that implements the spec. Anything other than SERVING fails the indicator and the endpoint returns 503.

Can Terminus check Kafka?

Yes, through MicroserviceHealthIndicator.pingCheck with KafkaOptions: transport Transport.KAFKA and a client naming your brokers. Terminus sets producerOnlyMode so the probe does not join a consumer group and trigger a rebalance on every check.

Should the health endpoint be authenticated?

Leave it open and keep the body boring. A monitor cannot send your auth header, and an endpoint that returns nothing but a status and an indicator key leaks nothing worth protecting. Put a guard on any deeper diagnostics route instead.

Point it at your own URL and watch it for real.

Any public URL · checked every 15 s