healthcheck

healthcheck runs dependency probes concurrently and aggregates them into one HTTP endpoint, recovering panics and reporting per-check results.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/healthcheck"

Turns a list of dependency probes into one HTTP endpoint that reports whether the service can do its job.

h := healthcheck.NewHandler([]healthcheck.HealthCheck{
    healthcheck.New("database", db),
    healthcheck.New("redis", redisClient),
    healthcheck.New("s3", s3Client),
})

One method, and the clients already have it

The contract is HealthCheck(context.Context) error. Every client in this module implements it directly, so registration needs no adapter: redis, valkey, sqs, s3, sqlconn, ipify and slack.

HealthCheckFunc adapts a plain function for anything else.

Keeping the method on the client rather than in a separate probe registry matters because the probe then uses the same connection pool, credentials and timeouts as real traffic. A probe that opens its own connection tests a path production never takes, and passes while the pool it was meant to check is exhausted.

Aggregation

  • Checks run in parallel, so total latency is the slowest check rather than the sum.
  • All pass: HTTP 200. Any fails: HTTP 503.
  • The body always carries per-check results, mapping each ID to OK or the error message, even on success.
  • A checker that panics is recovered, logged, and reported as failed.
  • With WithTimeout, a check that overruns is reported as failed.

Reporting every check on both outcomes is what makes the endpoint useful during an incident: one request tells you which dependency is down, without correlating logs.

Panic recovery matters more than it looks. A probe is code, usually the least-exercised code in the service. A nil map or a nil pointer in one check would otherwise take down the health handler entirely, which an orchestrator reads as the whole service being unhealthy.

Timeouts bound a probe, not a dependency

WithTimeout caps how long a check may take before being reported as failed.

Without it the endpoint is only as responsive as the slowest dependency, and a hung TCP connection to one of them can hold the health handler open past the orchestrator’s own probe timeout. The orchestrator then sees a timeout rather than a 503, which is a less specific signal and, for a liveness probe, one that restarts the pod.

The timeout should sit comfortably under whatever the platform allows for the probe.

HTTP dependency probes

CheckHTTPStatus builds a checker for an HTTP dependency, with WithConfigureRequest to add headers or authentication and WithAcceptStatus to decide which codes count as healthy.

WithAcceptStatus is needed more often than expected: an endpoint that returns 401 without credentials is still reachable, and for a liveness probe that may be exactly the evidence you want.

Check IDs must be unique and non-empty

Results are keyed by ID, so duplicates or empty IDs collapse into a single entry and quietly hide a check. Construction logs a warning when it sees one.

Liveness against readiness

Both probes hitting the same endpoint is a reliable way to cause an outage.

Readiness asks whether the service can serve traffic, so dependency checks belong there: failing readiness removes the instance from the load balancer until the dependency recovers.

Liveness asks whether the process is broken and must be restarted. Dependency checks do not belong there. A database blip would otherwise restart every replica simultaneously, and restarting does not fix a database. jsendx provides a ping handler with no dependency checks for that side.

Dependencies

This package reaches no external module: it uses only the Go standard library.