Introduction
Health endpoints let orchestrators like Kubernetes know whether your service is alive and ready to receive traffic. They are the foundation of self-healing infrastructure and zero-downtime deployments.
Key Concepts
- Liveness probe: Answers "Is the process alive?" — a failure triggers a container restart.
- Readiness probe: Answers "Can this instance handle traffic?" — a failure removes the instance from the load balancer.
- Startup probe: Answers "Has the application finished starting?" — prevents liveness checks from killing slow-starting apps.
- Dependency checks: Readiness probes typically verify database and cache connectivity before reporting ready.
Real World Context
Without health checks, a crashed service keeps receiving traffic (no liveness probe) or an instance with a dead database connection gets requests it cannot serve (no readiness probe). Health endpoints are required for any containerized production deployment.
Deep Dive
A liveness probe is a minimal endpoint that returns 200 if the process is running.
gomux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) })
This endpoint should have no external dependencies — its only job is to confirm the process has not deadlocked.
A readiness probe checks external dependencies like the database.
gofunc readinessHandler(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() if err := db.PingContext(ctx); err != nil { http.Error(w, "Database unavailable", http.StatusServiceUnavailable) return } w.WriteHeader(http.StatusOK) w.Write([]byte("ready")) } }
The 2-second timeout prevents the health check itself from hanging if the database is unresponsive.
Kubernetes configures these probes in the pod spec.
yamllivenessProbe: httpGet: path: /healthz port: 8080 readinessProbe: httpGet: path: /ready port: 8080
Kubernetes calls these endpoints periodically and reacts to failures automatically.
Common Pitfalls
- Adding database checks to the liveness probe — A database outage restarts all your pods, making the situation worse. Keep liveness probes dependency-free.
- No timeout on readiness checks — A hanging database connection blocks the health check goroutine indefinitely.
Best Practices
- Keep liveness probes trivial — Return 200 unconditionally. Only check process health, not dependencies.
- Use readiness probes for dependency checks — This removes unhealthy instances from the load balancer without restarting them.
Summary
- Liveness probes check if the process is alive; readiness probes check if it can serve traffic.
- Keep liveness probes dependency-free to avoid cascading restarts.
- Use timeouts on readiness probes to prevent hanging health checks.
Code Examples
type HealthResponse struct {
Status string `json:"status"`
Checks map[string]string `json:"checks"`
}
func healthHandler(db *sql.DB, cache *redis.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
health := HealthResponse{
Status: "ok",
Checks: make(map[string]string),
}
if err := db.Ping(); err != nil {
health.Status = "degraded"
health.Checks["database"] = err.Error()
} else {
health.Checks["database"] = "ok"
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(health)
}
}