Table of contents
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
OKor 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.
Links
- Reference documentation
- Source code
- Liveness handler: jsendx
- Mounting it: httpserver
- All nurago packages