# The health library turns a liveness probe into a dependency report

> health is a small Go library that serves a health endpoint as an http.Handler, runs your dependency checks, and returns an aggregated status with per-component detail, a 503 when something is down, a one second result cache and optional periodic checks so probes stay cheap. It is MIT licensed, it still sits at version 0.8.1, and the last tagged release was in June 2025.

**alexliesenfeld/health** — A simple and flexible health check library for Go.

- Repository: https://github.com/alexliesenfeld/health
- Stars: 834 · Forks: 45
- Language: Go
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/alexliesenfeld-health

## A handler that reports on dependencies

health is described as a simple and flexible health check library for Go, and the README is clear about the difference between it and the usual endpoint.

The library provides an http.Handler that acts as a health endpoint, usable by cloud infrastructure or other services to determine the availability of an application. Rather than simply returning a response with HTTP status code 200, it lets you build health checks that test the availability of all required dependencies. The HTTP response contains the aggregated health result and details about the health status of each component.

That is the whole idea, and it is a small one. A liveness endpoint that always returns 200 tells the orchestrator that the process is alive, which is useful but thin. What operators usually want at the readiness endpoint is a per-dependency answer, so a dashboard can show that the database is fine and the search index is not. This library is the machinery for that, in a form that any Go service can mount with a single line.

It is MIT licensed, the module is github.com/alexliesenfeld/health, and it requires Go 1.18 or newer, with testify as the only direct dependency and nothing else in the runtime graph. The repository is flat and readable: check.go, config.go and handler.go at the top level, plus interceptors and middleware directories, and an examples directory with separate projects for compatibility, hooks, kubernetes, listeners, the all-features example and simple cases.

The release history is worth noting for a dependency: v0.7.0 in May 2023, v0.8.0 in October 2023 and v0.8.1 in June 2025, with the last push to the main branch on 2026-06-15. There is still no 1.0 release, so the API can move.

## Synchronous checks run on every request

There are two kinds of check, and the first one is the simple one.

With synchronous health checks, every HTTP request initiates a health check and waits until all check functions complete before returning an aggregated health result. You configure them with the WithCheck option, and each check is a struct with a name, an optional timeout and a function that returns an error, where an error means the component is down.

The README is honest about when this is the wrong choice. Synchronous checks can be sufficient for smaller applications but might not scale well for more involved applications. An application that needs to read a large amount of data, that can experience latency issues, or that makes an expensive calculation to tell something about its health will not be able to respond quickly to health check requests. The README links to a piece on Kubernetes readiness probes and the service disruptions they cause.

The per-check timeout is the second line of defence, and there is a global one too. The WithTimeout option sets a timeout applied to all checks, and each individual check can carry its own Timeout, so a slow dependency can be given a shorter budget than the global default without affecting the others. The context passed to the check function carries a deadline according to the configured timeouts, which is what lets a check function bail out instead of hanging the probe request.

That last point matters more than it looks. A health endpoint that can itself block is a health endpoint that can take your service out of rotation, so the timeout configuration is not decoration.

## Periodic checks and the one second cache

The second kind of check answers the scaling problem.

Rather than executing health check functions on every HTTP request, periodic or asynchronous checks execute the check function on a fixed schedule. The health status is always read from a local cache that is regularly updated in the background, which allows responding to HTTP requests instantly without waiting for check functions to complete. Periodic checks are configured with the WithPeriodicCheck option, which takes the interval, an initial delay and the check itself.

The example in the README makes the timing explicit: a check that runs periodically every 15 seconds, started with an initial delay of 3 seconds, and which is not executed for each HTTP request.

Caching sits underneath both kinds. Health check results are cached to avoid sending too many requests to the services that your program checks and to mitigate denial of service attacks. The TTL is set to 1 second by default, and the README notes you can disable caching altogether, though the sentence describing that option is cut off in the text available here.

The library allows you to mix synchronous and asynchronous check functions, so the pattern the README recommends is to start out simple and transition into a more scalable implementation later. That is a useful property for an adoption decision, because a service that begins with one WithCheck and one WithPeriodicCheck can grow into a mixed setup without a rewrite.

## Wiring the endpoint into a Go service

The install is a module reference, and the configuration is a chain of options passed to the constructor.

```bash
go get github.com/alexliesenfeld/health
```

A checker is built with health.NewChecker, and each option configures one aspect:

```go
checker := health.NewChecker(
    health.WithCacheDuration(1*time.Second),
    health.WithTimeout(10*time.Second),
    health.WithCheck(health.Check{
        Name:    "database",
        Timeout: 2 * time.Second,
        Check:   db.PingContext,
    }),
)
```

The handler is then mounted on any path of a standard mux, and the README notes that you can pass further configuration options to NewHandler to modify the default configuration:

```go
http.Handle("/health", health.NewHandler(checker))
```

The check function itself is any function that returns an error, which is why the example can pass db.PingContext directly. For a check that needs more than a call, you write a function taking a context and returning an error, and returning a non-nil error means the component is considered unavailable or down.

There is one more option in the example worth reading, the status listener, which is invoked when the health status changes and takes the context and the checker state:

```go
health.WithStatusListener(func(ctx context.Context, state health.CheckerState) {
    log.Println(fmt.Sprintf("health status changed to %s", state.Status))
}),
```

The README notes that more hooks are also available and points to the documentation for them.

## The response contract: 503 and per-component detail

The response shape is the part of the contract that matters to whoever calls your endpoint, and the README gives it exactly.

Because a check in the example fails on purpose, a request to the health endpoint would yield an HTTP status code of 503, Service Unavailable, together with a JSON body like this:

```json
{
  "status": "down",
  "details": {
    "database": {
      "status": "up",
      "timestamp": "2021-07-01T08:05:14.603364Z"
    },
    "search": {
      "status": "down",
      "timestamp": "2021-07-01T08:05:08.522685Z",
      "error": "this makes the check fail"
    }
  }
}
```

Three details in that body are worth noting. The top-level status is the aggregate, so a caller that only wants a boolean can read one field. Each component carries its own status and a timestamp, which means a dashboard can show when each dependency last answered, not just whether it is currently green. And the failing component carries the error message, which is the difference between a health endpoint you can debug and one that only says no.

The 503 choice has consequences for how you deploy it. A readiness probe that returns 503 takes the instance out of the load balancer, which is usually what you want for a dependency that cannot serve traffic. The same handler used as a liveness probe will restart the process, which is usually not what you want, because a database outage would then restart every instance of every service that depends on it. The examples directory has a kubernetes project, and the README links to a discussion of readiness probe pitfalls, which is where the distinction is worth reading before you wire it up.

## Middleware, interceptors and the compatibility examples

The repository has two directories whose names hint at what the library does beyond the handler itself: middleware and interceptors.

The README's table of contents lists Middleware and Interceptors as its own section, alongside Compatibility With Other Libraries, Listening to Status Changes, Caching, and the synchronous versus asynchronous discussion. The interceptors directory is the natural place to look for what runs around a request, and the middleware directory for the pattern of wrapping a handler so the health response can be produced through an existing stack.

The examples directory is the best catalogue of intended use, with separate projects for compatibility, hooks, kubernetes, listeners, the all-features example and simple. The all-features example is described in the README as showing all features of the library, the kubernetes one is the deployment context, and listeners covers the status change hooks, which is the same subject as the WithStatusListener option in the getting started code.

The compatibility directory deserves a note of its own, and the name is spelled exactly that way in the repository listing. It exists because the library is not tied to a particular HTTP stack or framework, which is the point of handing you a plain http.Handler. A library that only worked with one router would be a much less interesting dependency, and the middleware and interceptors directories are how the project stays framework neutral while still offering the wrapping behaviour people want.

For the maintainers' side, the repository carries a CHANGELOG.md, a CODE_OF_CONDUCT.md and a CONTRIBUTING.md, and the build is run through a GitHub Actions workflow with codecov coverage reporting.

## Conclusion

Adopt health when your Go service has dependencies worth reporting on, when the platform that calls you is Kubernetes or something similar, and when you want one handler that works with any http.Server. Do not adopt it as a liveness signal by itself, because a dependency failure flips the whole endpoint, and remember the status is cached for a second by default, so a probe will not see a failure that appeared and disappeared inside that window. Before you adopt it, read the Kubernetes example under examples/kubernetes, because the difference between a readiness probe and a liveness probe decides whether an outage restarts your pods or takes them out of rotation.

## FAQ

### How do I add the health library to a Go project?

Run go get github.com/alexliesenfeld/health. The module requires Go 1.18 or newer, and its only direct dependency is testify for the test suite, so nothing else lands in your runtime dependency graph.

### What is the difference between WithCheck and WithPeriodicCheck?

WithCheck runs the check function on every HTTP request and waits for the results before answering, while WithPeriodicCheck runs it on a fixed schedule with an initial delay and the status is read from a cache refreshed in the background. The README says you can mix both, starting simple and moving to a more scalable setup later.

### What HTTP status does the health endpoint return when a check fails?

503 Service Unavailable, with a JSON body carrying the aggregated status and a details object per component, each with its own status, a timestamp and, for a failing component, the error message.

### How long are health check results cached?

The TTL is 1 second by default and is configured with the WithCacheDuration option. The README explains that caching exists to avoid sending too many requests to the services being checked and to mitigate denial of service attempts, and it can be disabled if you do not want it.

## Sources

- [alexliesenfeld/health on GitHub](https://github.com/alexliesenfeld/health)
- [Issues](https://github.com/alexliesenfeld/health/issues)
- [License: MIT](https://github.com/alexliesenfeld/health/blob/main/LICENSE)
- [README](https://github.com/alexliesenfeld/health/blob/main/README.md)
- [Releases](https://github.com/alexliesenfeld/health/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/alexliesenfeld-health
