go 145 lines · 3 tabs

Concurrent Readiness Health Check Endpoint in Go with Timeouts

Shared by codesnips Jul 2026
3 tabs
package health

import (
	"context"
	"sync"
	"time"
)

type Registry struct {
	checkers []Checker
	timeout  time.Duration
}

func NewRegistry(timeout time.Duration) *Registry {
	return &Registry{timeout: timeout}
}

func (r *Registry) Register(c Checker) {
	r.checkers = append(r.checkers, c)
}

func (r *Registry) Run(ctx context.Context) Health {
	results := make([]Result, len(r.checkers))
	var wg sync.WaitGroup

	for i, c := range r.checkers {
		wg.Add(1)
		go func(idx int, chk Checker) {
			defer wg.Done()
			cctx, cancel := context.WithTimeout(ctx, r.timeout)
			defer cancel()

			start := time.Now()
			err := chk.Check(cctx)
			res := Result{
				Name:    chk.Name(),
				Status:  StatusUp,
				Latency: time.Since(start) / time.Millisecond,
			}
			if err != nil {
				res.Status = StatusDown
				res.Error = err.Error()
			}
			results[idx] = res
		}(i, c)
	}

	wg.Wait()
	return Health{Status: aggregate(results), Checks: results}
}
3 files · go Explain with highlit

A readiness endpoint answers a deceptively simple question: is this service actually able to serve traffic right now? That means more than "the process is up" — it means the dependencies the service relies on (a database, a cache, an upstream API) are reachable. This snippet builds that endpoint in three collaborating files, with per-dependency timeouts, concurrent probing, and an aggregate status that maps cleanly onto HTTP semantics.

In checker.go, each dependency is expressed as a Checker interface with a single Check(ctx) method, so the health system does not care whether it is talking to Postgres, Redis, or an HTTP service. PingChecker adapts any func(ctx) error into a named checker, which keeps concrete probes small. The Result struct carries a Status, latency, and an optional error string, and Health aggregates results into an overall status where StatusDown for any single check degrades the whole to StatusDegraded. Splitting StatusUp from StatusDegraded lets an operator distinguish a totally broken service from one that lost a non-critical dependency.

In health.go, Registry.Run fans out across all registered checkers using a sync.WaitGroup, so a slow dependency does not serialize the others — total latency is bounded by the slowest single probe, not the sum. Each check receives its own context.WithTimeout, meaning a hung TCP connection cannot stall the whole endpoint; the derived context also inherits cancellation from the incoming request, so a client disconnect stops all in-flight probes. Timing is captured around the call to record real observed latency, which is useful for spotting a dependency that is up but degrading.

In handler.go, HealthHandler runs the registry and translates the aggregate into a status code: 200 for up or degraded, 503 for down. This distinction matters for orchestrators — Kubernetes should pull a 503 pod out of rotation, while a 200-degraded pod stays serving. The JSON body exposes per-check detail for dashboards and humans. A common pitfall this avoids is probing dependencies on the liveness path, which can cause cascading restarts; this handler is meant for readiness only. The pattern scales by registering more Checkers without touching the transport layer.

Share this code

Here's the card — post it anywhere.

Concurrent Readiness Health Check Endpoint in Go with Timeouts — share card
Link copied