Health Checks

Monitor the health of your application (memory, database, Redis, and custom services) through a structured JSON report.

Overview

@tekir/health gives you a composable health-check system. You register one or more checks, call health.run(), and get back a HealthReport that aggregates every result into a single isHealthy flag. Expose the report on a /health HTTP endpoint and your load balancer or orchestrator can automatically route traffic away from broken instances.

Installation

bun add @tekir/health

Health Class

Health is the central manager. Create one instance for your application, register checks, then call run() whenever you need a fresh report.

import { Health, MemoryHeapCheck, MemoryRSSCheck } from '@tekir/health'

const health = new Health()

health.register([
  new MemoryHeapCheck(),
  new MemoryRSSCheck()
])

register()

Accepts a single BaseCheck or an array of them. Calls are chainable; each call appends to the existing list.

// Register a single check
health.register(new MemoryHeapCheck())

// Register multiple checks at once
health.register([
  new MemoryHeapCheck(),
  new MemoryRSSCheck()
])

run()

Executes all registered checks in parallel and returns a HealthReport. The overall status is "error" if any check failed, "warning" if any check warned, and "ok" otherwise. Each check is bounded by a timeout (default 5000 ms, configurable via run({ timeout })); a check that exceeds it is reported as an error rather than hanging the whole report, and a single check that throws is isolated to its own error result. Pass run({ debug: true }) to include the internal debugInfo block (see Report Shape).

const report = await health.run()

console.log(report.isHealthy)  // true | false
console.log(report.status)     // 'ok' | 'warning' | 'error'
console.log(report.checks)     // array of individual check results

// Opt in to internal diagnostics (pid, platform, uptime, version).
// Only expose this on an authenticated/internal endpoint.
const internal = await health.run({ debug: true })
console.log(internal.debugInfo)

// Each check is bounded by a timeout (default 5000ms); a hung check
// is reported as an error instead of blocking the whole report.
const fast = await health.run({ timeout: 2000 })

Result

Every check's run() method must return a Result. Use the three static factory methods to create the appropriate outcome, and optionally attach diagnostic metadata with mergeMetaData().

import { Result } from '@tekir/health'

// Healthy
Result.ok('All systems nominal')

// Warning: still healthy, but worth monitoring
Result.warning('Heap 240MB above threshold')

// Error: marks the overall report as unhealthy
Result.failed('Cannot connect to database')

// Attach arbitrary metadata to any result
Result.ok('Heap 45MB').mergeMetaData({ heapMB: 45 })

Built-in Checks

MemoryHeapCheck

Reports the process V8 heap usage. Warns at 250 MB and fails at 300 MB by default. Both thresholds are configurable via warnWhenExceeds() and failWhenExceeds(), which accept raw byte counts or human-readable strings like "200mb".

import { MemoryHeapCheck } from '@tekir/health'

// Default thresholds: warn at 250 MB, fail at 300 MB
const check = new MemoryHeapCheck()

// Override with byte values
const check2 = new MemoryHeapCheck()
  .warnWhenExceeds(200 * 1024 * 1024)  // 200 MB
  .failWhenExceeds(256 * 1024 * 1024)  // 256 MB

// Override with human-readable strings
const check3 = new MemoryHeapCheck()
  .warnWhenExceeds('200mb')
  .failWhenExceeds('256mb')

MemoryRSSCheck

Reports the process Resident Set Size, the total memory the OS has allocated to the process including heap, stack, and native allocations. Warns at 320 MB and fails at 350 MB by default.

import { MemoryRSSCheck } from '@tekir/health'

// Default thresholds: warn at 320 MB, fail at 350 MB
const check = new MemoryRSSCheck()

const check2 = new MemoryRSSCheck()
  .warnWhenExceeds('300mb')
  .failWhenExceeds('400mb')

DbCheck

Verifies a database connection by running SELECT 1. Pass your database client instance as the first argument. Provide an optional connection name to distinguish multiple databases in the report; the check name becomes database:<connectionName>.

import { DbCheck } from '@tekir/health'
import { db } from '#services'

// Single default connection
health.register(new DbCheck(db))

// Named connection: the check name becomes 'database:primary'
health.register(new DbCheck(db, 'primary'))

RedisCheck

Verifies a Redis connection by inspecting the client's connectedproperty. Pass your Redis client instance as the first argument and an optional name for multi-Redis setups.

import { RedisCheck } from '@tekir/health'
import { redis } from '#services'

// Single connection
health.register(new RedisCheck(redis))

// Named connection: the check name becomes 'redis:cache'
health.register(new RedisCheck(redis, 'cache'))

Custom Checks

Extend BaseCheck, set a name string, and implement the run() method. It can be synchronous or async. Return a Result using the factory methods and optionally attach metadata with mergeMetaData().

import { BaseCheck, Result } from '@tekir/health'

class DiskSpaceCheck extends BaseCheck {
  name = 'disk:space'

  async run(): Promise<Result> {
    const proc = Bun.spawnSync(['df', '-k', '/'])
    const output = proc.stdout.toString()
    const match = output.match(/(\d+)%/)
    if (!match) return Result.warning('Could not parse disk usage')

    const used = parseInt(match[1])
    if (used > 90) return Result.failed(`Disk ${used}% full`).mergeMetaData({ usedPercent: used })
    if (used > 75) return Result.warning(`Disk ${used}% full`).mergeMetaData({ usedPercent: used })
    return Result.ok(`Disk ${used}% used`).mergeMetaData({ usedPercent: used })
  }
}

health.register(new DiskSpaceCheck())

Caching Results

Expensive checks (e.g. ones that open a TCP connection) can cache their last result for a configurable window. Call cacheFor() on any check instance. When a cached result is served, the isCached flag in the report is set to true.

import { MemoryHeapCheck } from '@tekir/health'

// Re-use a cached result for up to 30 seconds
const check = new MemoryHeapCheck().cacheFor('30s')

// Duration accepts: ms, s, m, h (e.g. '5m', '1h', 500)
const check2 = new MemoryRSSCheck().cacheFor('1m')

// When a cached result is returned, isCached === true in the report

HTTP Health Endpoint

The canonical pattern is a GET /health route that returns 200 when healthy and 503 when not. Orchestrators like Kubernetes and ECS can call this endpoint to decide whether to route traffic to a pod.

// start/routes.ts
import type { TekirApp } from '@tekir/core'
import { Health, MemoryHeapCheck, DbCheck } from '@tekir/health'
import { db } from '#services'

export default function({ router }: TekirApp) {
  const health = new Health()
  health.register([new MemoryHeapCheck(), new DbCheck(db)])

  router.get('/health', async ({ response }) => {
    const report = await health.run()
    return response.status(report.isHealthy ? 200 : 503).json(report)
  })
}

Report Shape

health.run() returns a HealthReport object. By default it carries only isHealthy, status, finishedAt, and the per-check checks array, so a public /health endpoint never leaks internal details.

// Default report: no debugInfo (opt in with run({ debug: true })).
{
  "isHealthy": true,
  "status": "ok",
  "finishedAt": "2026-03-25T12:00:00.000Z",
  "checks": [
    {
      "name": "memory:heap",
      "status": "ok",
      "message": "Heap 48MB",
      "isCached": false,
      "finishedAt": "2026-03-25T12:00:00.001Z",
      "meta": { "heapMB": 48 }
    },
    {
      "name": "database",
      "status": "ok",
      "message": "Connected",
      "isCached": true,
      "finishedAt": "2026-03-25T11:59:30.004Z"
    }
  ]
}

Pass run({ debug: true }) to add a debugInfo block with the process PID, platform, uptime in seconds, and Node/Bun version. Because this exposes runtime and platform details, only return it from an authenticated or internal endpoint.

// With run({ debug: true }), a debugInfo block is added.
// Keep this on an internal/authenticated endpoint only: it leaks
// the platform and runtime version, which aids reconnaissance.
{
  "isHealthy": true,
  "status": "ok",
  "finishedAt": "2026-03-25T12:00:00.000Z",
  "debugInfo": {
    "pid": 12345,
    "platform": "linux",
    "uptime": 3600,
    "version": "v22.0.0"
  },
  "checks": [ ... ]
}

Auto-configuration via Provider

Register HealthProvider in your kernel to get a shared Health instance in the container. Then register your checks in start/boot.ts:

start/boot.ts
import type { TekirApp } from '@tekir/core'
import { MemoryHeapCheck, DbCheck } from '@tekir/health'
import { db } from '#services'

export default function({ app }: TekirApp) {
  const health = app.use('health')
  health.register([
    new MemoryHeapCheck(),
    new DbCheck(db)
  ])
}