Skip to content

Health Check Protocol ​

Connectum implements the gRPC Health Checking Protocol with additional HTTP endpoints for load balancers and Kubernetes integration.

Configuration ​

Install @connectum/healthcheck with your package manager, then register Healthcheck() in the server's protocols array:

bash
npm install @connectum/healthcheck
bash
pnpm add @connectum/healthcheck
bash
bun add @connectum/healthcheck

The Healthcheck() factory function accepts the following options:

typescript
Healthcheck({
  httpEnabled: true,                              // Enable HTTP health endpoints
  httpPaths: ['/healthz', '/health', '/readyz'],  // HTTP endpoint paths
  watchInterval: 500,                             // Watch re-check interval (ms)
  manager: customManager,                         // Custom manager instance
})

Use httpEnabled and httpPaths for platform endpoints, watchInterval for the Watch re-check period (see Health.Watch), and manager when a test or multi-server process must avoid the singleton. See HealthcheckOptions for the exact field types and defaults.

Healthcheck() checks its options when it is called and throws instead of starting a server that misbehaves later:

OptionAcceptedOtherwise
optionsan object, or nothingTypeError (null, an array, a string)
httpEnabledtrue or falseTypeError ("yes", 1, null)
watchIntervalan integer from 1 to 2147483647RangeError for 0, a fraction, NaN, a negative number, Infinity or a larger number; TypeError for a string or null
managera HealthcheckManagerTypeError
httpPathssee Custom PathsRangeError or TypeError

An omitted option (or an explicit undefined) takes its default. One server answers grpc.health.v1.Health from one registration: a second Healthcheck() in protocols, or an application service that is already grpc.health.v1.Health, makes start() (and the first call made through localClient()) reject with an error naming the service. The check runs when the server builds its routes, not at createServer(). One Healthcheck() value can still be passed to several servers.

If a configuration that used to start now throws, see Healthcheck option checks.

The healthcheckManager Singleton ​

The healthcheckManager is a global singleton you import from anywhere in your application to update service health status:

typescript
import { healthcheckManager, ServingStatus } from '@connectum/healthcheck';

// Update overall health status
healthcheckManager.update(ServingStatus.SERVING);

// Update a specific service's health status
healthcheckManager.update(ServingStatus.SERVING, 'my.service.v1.MyService');

// Mark as unhealthy
healthcheckManager.update(ServingStatus.NOT_SERVING);

Every probe reads this one state:

One singleton serves one server

Every Healthcheck() without manager uses this singleton, and each server's start re-initializes it with only that server's services. Start a second server in the same process and every service the second server does not mount leaves the registry, including the first server's own. The first server keeps running, but it answers NOT_FOUND for those services. The overall check (empty service name) is computed over the one shared registry, so it also follows the other server's statuses: it reports NOT_SERVING while any entry is not SERVING. Neither server logs a warning. Give each server its own manager, as shown in Isolated Manager for Testing.

ServingStatus Values ​

StatusValueDescription
UNKNOWN0Status is unknown (initial state)
SERVING1Service is healthy and accepting requests
NOT_SERVING2Service is unhealthy or draining connections
SERVICE_UNKNOWN3Requested service is not registered

Manager Methods ​

MethodDescription
update(status, service?)Update status. Without service, updates all registered entries (services and components). Throws on an unknown name.
register(component, initialStatus?)Register an application health component (default UNKNOWN). Re-registering preserves the current status.
set(component, status)Set a component's status (upsert: registers the component if absent).
subscribe(listener)Call listener (no arguments) after every change of the registry; returns a function that removes it. Health.Watch is built on this. A listener must not change the registry: calling update, register, set, unregister, initialize or clear from inside one throws, so every listener sees the state of the change that notified it.
unregister(component)Remove a registered component.
getStatus(service)Get status of a specific service or component. Returns ServiceStatus | undefined (undefined for an unknown name — does not throw).
getAllStatuses()Get a Map of all service and component statuses
areAllHealthy()Check if all services and components report SERVING
initialize(serviceNames)Initialize the RPC service slice (called by the protocol's setup and again by its ready, which passes every mounted service; in-process transports do not re-run either, a retry after a failed route materialization does). Replaces only service entries; never touches components.
clear()Clear all services and components

update(), register() and set() accept only the four ServingStatus values above. A value of another type (a string such as "SERVING", null, undefined) throws TypeError; a number outside 0 to 3 (a fraction, NaN, 99) throws RangeError; a component or service name that is not a string throws TypeError. The registry is left as it was, so Health.Check and Health.List keep answering.

gRPC Health Check Protocol ​

The package implements three gRPC methods on the grpc.health.v1.Health service:

The grpcurl commands below require Reflection() and a plaintext HTTP/2 listener (allowHTTP1: false). See Server reflection for setup. Use the HTTP examples with the default plaintext HTTP/1.1 listener.

Health.Check ​

Returns the current health status:

bash
# Check overall health
grpcurl -plaintext localhost:5000 grpc.health.v1.Health/Check

# Check a specific service
grpcurl -plaintext \
  -d '{"service": "my.service.v1.MyService"}' \
  localhost:5000 grpc.health.v1.Health/Check

A service the manager does not track answers NOT_FOUND. The message repeats at most the first 128 characters of the requested name, followed by … when the name was longer, so a request with a very long name cannot build an oversized response.

Health.Watch ​

Streams health status changes in real time:

bash
grpcurl -plaintext localhost:5000 grpc.health.v1.Health/Watch

Behavior follows the gRPC specification:

  • Immediately sends the current status
  • Sends one message for every change of the status, in the order the changes happened, and nothing while the status holds
  • For unknown services, sends SERVICE_UNKNOWN (does not terminate the call)
  • Terminates when the client disconnects

Changes come from the manager's notifications (subscribe()), not from sampling. A change reaches the client as soon as it is made, and a status that goes NOT_SERVING and back to SERVING inside one watchInterval is delivered as two messages. For that reason a watcher also sees short intermediate states, for example UNKNOWN between a service being registered and its first update(). watchInterval (default 500 ms) is only the period of a re-check that also finds a state change the manager did not announce.

A client that stops reading while its status keeps changing makes the server hold one pending message per change for that call; the messages are not dropped, because dropping them would hide transitions.

Health.List ​

List is part of the gRPC health protocol, alongside Check and Watch. It returns a snapshot of the tracked service statuses.

Lists all tracked services with their statuses. Healthcheck tracks your services and the services of custom protocols, wherever Healthcheck() stands in protocols. Each starts as UNKNOWN and counts in the overall status. It never tracks its own grpc.health.v1.Health service or the reflection services (grpc.reflection.v1 and v1alpha), so Reflection() changes neither List nor the overall status. A service that is only declared — for example next to a mounted service in the same .proto file — is not tracked, and Check answers NOT_FOUND for it:

bash
grpcurl -plaintext localhost:5000 grpc.health.v1.Health/List

HTTP Health Endpoints ​

When httpEnabled: true, the following HTTP endpoints are available:

Default Endpoints ​

PathDescription
/healthzOverall health status
/healthOverall health status (alias)
/readyzOverall health status (alias; no separate readiness state)

Response Format ​

json
{
  "status": "SERVING",
  "service": "overall",
  "timestamp": "2026-02-12T10:30:00.000Z"
}

HTTP Status Codes ​

ServingStatusHTTP Code
SERVING200 OK
NOT_SERVING503 Service Unavailable
UNKNOWN503 Service Unavailable
SERVICE_UNKNOWN404 Not Found

Check a Specific Service via HTTP ​

bash
curl 'http://localhost:5000/healthz?service=my.service.v1.MyService'

The service query parameter selects one service or component. The answer depends on the URL alone: the Host header of the request, including an empty or malformed one, does not change which service the response describes.

RequestResponse
No service parameterOverall health; the body carries "service": "overall"
?service= (empty value)The overall status and HTTP code, as if the parameter were absent; the body carries "service": "overall"
?service=<known name>The status of that service or component, with its name in the service field
?service=<unknown name>404 Not Found, status SERVICE_UNKNOWN, with the requested name in the service field

Custom Paths ​

typescript
Healthcheck({
  httpEnabled: true,
  httpPaths: ['/healthz', '/ready', '/live'],
})

httpPaths replaces the default list. It must be a non-empty array of absolute paths: each entry starts with / and contains no ? or #. Healthcheck() checks it whenever it is passed, also with httpEnabled: false, and throws instead of starting a server that serves nothing:

ValueResult
not passed (undefined)The default paths /healthz, /health, /readyz
[]RangeError. To turn the HTTP endpoint off, set httpEnabled: false
an entry that is not a string, has no leading /, or contains ? or #TypeError

A request matches a configured path only when the path is identical:

  • The comparison is exact and case-sensitive. /healthz/ and /Healthz do not match /healthz; such a request is not handled by the health endpoint. To serve both forms, list both.
  • The query string is ignored when matching, so /healthz?service=my.service.v1.MyService matches /healthz.
  • The request method is not checked.

createHttpHealthHandler(manager, healthPaths), the exported factory behind the endpoint, applies the same rules to healthPaths (RangeError for [], TypeError for a bad entry) and serves the default paths when the argument is omitted.

If you passed httpPaths: [] before 1.3, see HTTP health options validation.

Custom Dependency Health Checks ​

Monitor downstream dependencies (databases, external APIs) alongside your service health:

typescript
import { healthcheckManager, ServingStatus } from '@connectum/healthcheck';

// Application components survive the protocol's RPC-service initialization.
// Component names must be non-empty and contain no dots.
healthcheckManager.register('database');
healthcheckManager.register('cache');

// Periodically check database health
setInterval(async () => {
  try {
    await db.ping();
    healthcheckManager.set('database', ServingStatus.SERVING);
  } catch {
    healthcheckManager.set('database', ServingStatus.NOT_SERVING);
  }
}, 10000);

// Check cache health
setInterval(async () => {
  try {
    await redis.ping();
    healthcheckManager.set('cache', ServingStatus.SERVING);
  } catch {
    healthcheckManager.set('cache', ServingStatus.NOT_SERVING);
  }
}, 10000);

Start these checks only when the dependencies are connected, and clear their intervals in a shutdown hook. Mark each RPC service SERVING separately in the ready listener: an unqualified update(SERVING) also marks dependency components healthy, even before their checks succeed.

Worker-only processes ​

A process with no RPC services (services: []) still needs an explicit readiness source, because an empty registry is never healthy. Register a component before start() and flip it with the server lifecycle:

typescript
import { createServer } from '@connectum/core';
import { Healthcheck, healthcheckManager, ServingStatus } from '@connectum/healthcheck';

const server = createServer({
  services: [],
  port: 5000,
  protocols: [Healthcheck({ httpEnabled: true })],
});

healthcheckManager.register('process');
server.on('ready', () => healthcheckManager.set('process', ServingStatus.SERVING));
server.on('stopping', () => healthcheckManager.set('process', ServingStatus.NOT_SERVING));

await server.start();

/healthz then returns 200 once the component is SERVING, and gRPC Check for process reports the same status, which is what a docker compose service_healthy condition or a Kubernetes probe needs.

Use areAllHealthy() for aggregate health status:

bash
curl http://localhost:5000/healthz
# Returns 503 if ANY dependency reports NOT_SERVING

Isolated Manager for Testing ​

Use createHealthcheckManager() to create isolated instances for tests or multi-server setups. A process that runs two servers must pass a separate manager to each Healthcheck(), or the second start removes the first server's services from the shared singleton (see The healthcheckManager Singleton):

Bun

Import the runner from bun:test instead of node:test and run the file with bun test.

typescript
import { describe, it } from 'node:test';
import assert from 'node:assert';
import {
  Healthcheck,
  createHealthcheckManager,
  ServingStatus,
} from '@connectum/healthcheck';
import { createServer } from '@connectum/core';

describe('health check', () => {
  it('should track service status', () => {
    const manager = createHealthcheckManager();
    manager.initialize(['my.service.v1.MyService']);

    manager.update(ServingStatus.SERVING, 'my.service.v1.MyService');
    assert.ok(manager.areAllHealthy());

    manager.update(ServingStatus.NOT_SERVING, 'my.service.v1.MyService');
    assert.ok(!manager.areAllHealthy());
  });

  it('should work with createServer', async () => {
    const manager = createHealthcheckManager();

    const server = createServer({
      services: [routes],
      protocols: [Healthcheck({ httpEnabled: true, manager })],
    });

    server.on('ready', () => {
      manager.update(ServingStatus.SERVING);
    });

    await server.start();
    // ... test assertions ...
    await server.stop();
  });
});