Library / SDK
AppsFlyer/go-sundheit avatar
AppsFlyer/go-sundheit

go-sundheit, background health checks and a health endpoint for Go services

A library built to provide support for defining service health for golang services. It allows you to register async health checks for your dependencies and the service itself, provides a health endpoint that exposes their status, and health metrics.

560 stars32 forksGoApache-2.0

At a glance

What is it?
go-sundheit is AppsFlyer's Apache-2.0 library for defining service health in Go: you register asynchronous checks for your dependencies and for the service itself, schedule them with an initial delay, an execution period and a timeout, and serve the aggregated status from an HTTP endpoint. It ships HTTP, DNS and ping checks and a two-method interface for writing your own.
Who is it for?
go-sundheit suits a Go service that wants a health endpoint that reflects real dependency probes rather than a static liveness flag, and it suits a team that wants check results as metrics as well as an HTTP response. It does not suit a project that needs Kubernetes-style startup, readiness and liveness probes separated, since the library exposes one health endpoint with one aggregated status.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 101 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A health service, not a health endpoint library

go-sundheit is described as a library built to provide support for defining service health for Go services. It lets you register async health checks for your dependencies and for the service itself, and provides a health endpoint that exposes their status.

The name comes from the German word Gesundheit, meaning health, and the README notes the pronunciation. The project comes from AppsFlyer, and the licence is Apache-2.0.

What makes it a service rather than a helper is the scheduling model. Checks do not run when someone asks for the endpoint; they run on a timer in the background, and the endpoint reports the last known result. That distinction matters more than it sounds. A health endpoint that probes dependencies synchronously on request is a health endpoint that can be slow, that can fail under load for reasons unrelated to the service, and that tells you about the state of the world at the instant of the request rather than the state of the service you actually want to know about.

The API is small. `gosundheit.New()` creates a health instance, with optional configuration functions passed as `Option` values. Every option is marked with the prefix `WithX`, and the README lists the two that exist: `WithCheckListeners`, which lets you act on check registration, start and completed events, and `WithHealthListeners`, which lets you act on changes in the health service results.

That second option is the interesting one. A check listener fires per check, and a health listener fires when the aggregated result changes. The second is what you would use to emit a metric or trigger an alert on a transition, which is a different signal from the per-check stream and much cheaper to consume.

The repository layout is flat and readable: `check.go`, `check_listener.go`, `check_task.go`, `health.go`, `health_listener.go`, `config.go`, `options.go`, `types.go` and `utils.go` at the root, with `checks/` for the built-in checks, `http/` for the HTTP handler, `opencensus/` for the metrics integration, `docs/` and `test/`.

The last push to the repository was on 2026-07-14, and the recorded releases are v0.6.0 from 2024-07-25, titled Deprecate github.com/pkg/errors, v0.5.0 from 2021-08-15, titled Improved error handling for check registration, and v0.4.0 from 2021-05-25, titled Major API improvements.

Registration carries the schedule

A check is registered with the health instance along with the timing options that govern it, and the three that appear in the documentation are the ones you will use every time.

`InitialDelay` is how long before the first run, `ExecutionPeriod` is the interval between runs, and `ExecutionTimeout` bounds a single execution. The full usage example registers an HTTP check with a one second initial delay and a ten second period.

go
	err = h.RegisterCheck(
		httpCheck,
		gosundheit.InitialDelay(time.Second),         // the check will run once after 1 sec
		gosundheit.ExecutionPeriod(10 * time.Second), // the check will be executed every 10 sec
	)

The `InitialDelay` option exists because of what health checks usually probe. A dependency that has just come up is often not ready yet, and a check that runs immediately at registration will fail and, depending on how the endpoint aggregates, report the service as unhealthy during a period when it is actually fine. Delaying the first run by a second or two removes that false negative.

The timeout is what stops a slow dependency from consuming the health service's goroutines indefinitely, and it is set per check rather than globally, which is right because a database check and an HTTP check to a cache have different sensible budgets.

The example also shows the check construction separated from registration, which matters for the error handling.

go
	// define an HTTP dependency check
	httpCheckConf := checks.HTTPCheckConfig{
		CheckName: "httpbin.url.check",
		Timeout:   1 * time.Second,
		// dependency you're checking - use your own URL here...
		// this URL will fail 50% of the times
		URL:       "http://httpbin.org/status/200,300",
	}

The check is created with a constructor that returns an error, and the README is emphatic about it: fail fast when you misconfigured the URL, don't ignore errors. There is also a `checks.Must` variant that panics instead, and the documentation shows both, with `Must` presented as the alternative to panicking when creating a check fails.

Two things about the example URL are worth noticing as a teaching device. The status list `200,300` means the check accepts either code, and the comment says the URL will fail 50% of the times. It is a deliberately flaky dependency, which is how you can exercise the failing path of your health endpoint without unplugging anything.

The built-in checks cover HTTP, DNS and reachability

The library ships a set of built-in checks, and the three families cover most of what a service depends on.

The HTTP check triggers an HTTP request to one of your dependencies, verifies the response status, and optionally checks the content of the response body. That last part is what makes it a dependency check rather than a reachability check: a dependency that answers 200 with an error page is broken, and only inspecting the body catches it.

The DNS checks perform a lookup of a hostname, domain name, CNAME or similar, and validate that it resolves to at least the minimum number of required results. The simple form takes a host and a count.

go
// Schedule a host resolution check for `example.com`, requiring at least one results, and running every 10 sec
h.RegisterCheck(
	checks.NewHostResolveCheck("example.com", 1),
	gosundheit.ExecutionPeriod(10 * time.Second),
)

The lower-level form takes a custom lookup function, which is how you do something other than a forward hostname resolution. The README's example is a reverse DNS lookup.

go
func ReverseDNLookup(ctx context.Context, addr string) (resolvedCount int, err error) {
	names, err := net.DefaultResolver.LookupAddr(ctx, addr)
	resolvedCount = len(names)
	return
}

The ping checks verify that a resource is still alive and reachable, and there are two forms. The first takes anything that implements the `Pinger` interface, which `sql.DB` does, so a database check is a one-liner.

go
	dbCheck, err := checks.NewPingCheck("db.check", db)
	_ = h.RegisterCheck(&gosundheit.Config{
		Check: dbCheck,
		// ...
	})

The second takes a pinger you build yourself, and the README shows a TCP dialer for a generic connection test.

go
	pinger := checks.NewDialPinger("tcp", "example.com")
	pingCheck, err := checks.NewPingCheck("example.com.reachable", pinger)
	h.RegisterCheck(pingCheck)

The `NewDialPinger` function supports all the network and address parameters supported by `net.Dial()`, so you are not limited to TCP. That is the check to reach for when a dependency is reachable but speaks a protocol that the ping check does not understand.

The set is small by design. Anything else, a queue depth, a disk space check, a licence expiry check, is a custom check, and the interface for that is two methods.

Two ways to write a custom check

The custom check API is the part of the library you will use most, and there are two routes depending on how much you need.

The interface is the definition of the whole system, and it is small.

go
// Check is the API for defining health checks.
// A valid check has a non empty Name() and a check (Execute()) function.
type Check interface {
	// Name is the name of the check.
	// Check names must be metric compatible.
	Name() string
	// Execute runs a single time check, and returns an error when the check fails, and an optional details object.
	Execute() (details interface{}, err error)
}

Two methods and one constraint that catches people out. The `Name()` method must return a non-empty name, and check names must be metric compatible, because the names are used as metric identifiers. That means no spaces and no characters that a metric backend would reject, and it is a constraint on naming that is easy to ignore until you try to export the results.

`Execute()` runs a single check and returns an error when it fails, plus an optional details object. The details object is the more useful of the two, because a check that only says pass or fail tells you little. The example lottery check returns a formatted string with the value it rolled, so a failing check carries the evidence of why.

The first route is the `CustomCheck` struct, which the README describes as the simplest way to implement a check if all you need is to define a check function.

go
func lotteryCheck() (details interface{}, err error) {
	lottery := rand.Float32()
	details = fmt.Sprintf("lottery=%f", lottery)
	if lottery < 0.5 {
		err = errors.New("Sorry, I failed")
	}
	return
}

You then register it with the struct, the check name and the function, plus the schedule options.

go
h.RegisterCheck(
	&checks.CustomCheck{
		CheckName: "lottery.check",
		CheckFunc: lotteryCheck,
	},
	gosundheit.InitialDelay(0),
	gosundheit.ExecutionPeriod(2 * time.Minute), 
	gosundheit.ExecutionTimeout(5 * time.Second)
)

Note the `InitialDelay(0)` there, which runs the check right away, and the `ExecutionTimeout` of five seconds, which bounds a function that in this case returns immediately but might not in the general case.

The second route is implementing the `Check` interface directly, which the README describes as sometimes necessary. The difference in approach is that `CustomCheck` gives you a function and no state, while the interface lets you build a type that holds state, has a lifecycle, or is shared between checks. The ping checks use that route, because a pinger is a thing with a configuration rather than a function.

One detail in the example worth noticing is the use of `github.com/pkg/errors`. The v0.6.0 release is titled Deprecate github.com/pkg/errors, and while the module file still lists it as a requirement, the direction of travel is that new code should use the standard library errors package.

The endpoint, the metrics and the two listener hooks

Serving the results is one line, and it is an ordinary `http.Handler`.

go
	// register a health endpoint
	http.Handle("/admin/health.json", healthhttp.HandleHealthJSON(h))
	
	// serve HTTP
	log.Fatal(http.ListenAndServe(":8080", nil))

The path is the conventional admin path, and the handler comes from the `http` subpackage as `HandleHealthJSON`, so the response format is JSON. The fact that it plugs into the standard `http` mux rather than requiring its own server is a small design choice with real consequences: it means the health endpoint can share the port and the TLS configuration of the rest of the service, and it means the endpoint is available wherever you mount the mux.

The metrics side is the `opencensus/` directory in the repository, which is the OpenCensus integration, so check results can be exported as metrics rather than only read over HTTP. The names used for checks double as metric identifiers, which is why the interface comment insists they must be metric compatible.

The two listener options then let you build on top of both channels. `WithCheckListeners` fires on check registration, start and completed events, so you can log every probe or count them. `WithHealthListeners` fires when the health service results change, which is the transition signal: the service went from healthy to unhealthy, or the other way.

That distinction between a per-check event and a per-result-change event is worth keeping in mind when wiring an alerting path. A check that fails every ten seconds for an hour produces 360 check events and zero health change events after the first one, so a listener on the health results is the cheaper and more meaningful signal.

The dependency situation is small, which suits a library that most services will pull in alongside their framework. The module requires `github.com/fortytw2/leaktest` at v1.3.0, `github.com/pkg/errors` at v0.8.1 and `github.com/stretchr/testify` at v1.6.1, all test-time dependencies, and the module itself declares Go 1.24.

The build, the release history and where it fits

The Makefile is three targets and a pinned linter, and it shows what the project's own quality bar is.

makefile
all: build lint test

build: 
	##### building #####
	go build -v

The lint target installs a specific golangci-lint version, v2.12.2, into a `bin` directory on demand using the upstream install script, and the test target runs the suite with the race detector enabled.

makefile
test: build
	##### testing #####
	go test $(testflags) -v -race ./...

The race detector on a library that runs checks concurrently in the background is the right call, because that is where a race would live. The repository also carries a `.golangci.yml` and CI badges for GitHub Actions and CircleCI, plus a Go Report Card badge and a coveralls badge.

The release history is the part to weigh. v0.6.0 came out on 2024-07-25 and its title is a deprecation rather than a feature. The two before it, v0.5.0 and v0.4.0, are from 2021, one titled Improved error handling for check registration and one titled Major API improvements. The last push to the repository was on 2026-07-14.

So the pattern is that the API changed substantially in 2021 and has been stable since, with one maintenance release two years ago and work continuing on the branch. For a library whose job is to sit inside someone else's service and report status, that stability is a reasonable thing to have, and the risk is not churn but stagnation, so the question for a new user is whether the interfaces you need are in the version you take.

The alternatives are worth naming concretely. Kubernetes-style probes in most Go HTTP frameworks are simpler, a liveness endpoint that returns 200 while the process is alive and a readiness endpoint that flips based on a flag you set yourself. A full observability stack with an exporter and a time-series backend gives you the same data with alerting and history, at the cost of much more machinery. And a dependency on the standard library's `net/http` with a hand-written handler is the zero-dependency option, which is fine until you have more than two checks.

The difference in approach is that go-sundheit owns the scheduling, the aggregation and the listener hooks, and gives you a small interface for writing probes. If you already run a full observability stack, the health endpoint is a nice-to-have rather than a need, and the metrics are the part that actually adds something.

Editorial conclusion

go-sundheit suits a Go service that wants a health endpoint that reflects real dependency probes rather than a static liveness flag, and it suits a team that wants check results as metrics as well as an HTTP response. It does not suit a project that needs Kubernetes-style startup, readiness and liveness probes separated, since the library exposes one health endpoint with one aggregated status. Verify first that the check names you choose are metric compatible, since the interface requires it, that the version you take matches the Go toolchain in your build, and that you have read the deprecation of `github.com/pkg/errors` if your code still imports it.

Frequently asked questions

What is go-sundheit?

It is a Go library from AppsFlyer for defining service health. You register asynchronous checks for your dependencies and for the service itself, and it provides a health endpoint that exposes their status, with health metrics through its OpenCensus integration.

How do I install go-sundheit?

With Go modules, using go get github.com/AppsFlyer/[email protected]. The module declares Go 1.24, and its only requirements are test-time dependencies including testify, leaktest and pkg/errors.

What built-in checks does go-sundheit provide?

An HTTP check that triggers a request and verifies the response status and optionally the body, DNS checks that resolve a hostname and validate the number of results, with a lower-level form taking a custom lookup function, and ping checks that verify a resource is reachable, including one for a database through the Pinger interface and one built on a TCP dialer.

How do I write a custom health check?

Implement the Check interface, which requires a Name method returning a metric-compatible name and an Execute method returning an optional details object and an error, or use the checks.CustomCheck struct with a name and a function if you do not need a stateful type. Register it with InitialDelay, ExecutionPeriod and ExecutionTimeout options.

How is the health status exposed?

Through the http subpackage, by registering healthhttp.HandleHealthJSON(h) on the standard HTTP mux, for example at /admin/health.json, and serving it with the rest of your service. The WithCheckListeners and WithHealthListeners options let you act on check events and on changes in the aggregated results.

Official sources

  1. AppsFlyer/go-sundheit on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/appsflyer-go-sundheit.svg)](https://hysenlabs.com/projects/appsflyer-go-sundheit)

Was this useful?