Health, Readiness & Liveness Probes
Separate liveness (is the process alive?) from readiness (can this instance serve traffic?) so orchestrators and load balancers behave correctly during startup, dependency outages, and deploys.
Search across all documentation pages
Separate liveness (is the process alive?) from readiness (can this instance serve traffic?) so orchestrators and load balancers behave correctly during startup, dependency outages, and deploys.
Liveness probes restart stuck containers.
Readiness probes remove instances from service endpoints until they can handle requests.
Startup probes (Kubernetes 1.16+) protect slow-starting apps before liveness kicks in.
Probe handlers must be fast, idempotent, and free of auth middleware that blocks kubelet traffic.
Quick-reference recipe card - copy-paste ready.
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
if depsHealthy(r.Context()) {
w.WriteHeader(http.StatusOK)
return
}
http.Error(w, "not ready", http.StatusServiceUnavailable)
})When to reach for this:
package main
import (
"context"
"database/sql"
"errors"
"log/slog"
"net/http"
"sync"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
)
type App struct {
db *sql.DB
ready sync.RWMutex
isReady bool
}
func (a *App) setReady(v bool) {
a.ready.Lock()
a.isReady = v
a.ready.Unlock()
}
func (a *App) checkReady() bool {
a.ready.RLock()
defer a.ready.RUnlock()
return a.isReady
}
func (a *App) liveness(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
func (a *App) readiness(w http.ResponseWriter, r *http.Request) {
if !a.checkReady() {
http.Error(w, "starting", http.StatusServiceUnavailable)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
defer cancel()
if err := a.db.PingContext(ctx); err != nil {
http.Error(w, "db unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ready"))
}
func (a *App) business(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"orders":[]}`))
}
func main() {
db, err := sql.Open("pgx", "postgres://localhost:5432/app?sslmode=disable")
if err != nil {
slog.Error("db open failed", "err", err)
return
}
app := &App{db: db}
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", app.liveness)
mux.HandleFunc("GET /readyz", app.readiness)
mux.HandleFunc("GET /orders", app.business)
go func() {
time.Sleep(2 * time.Second)
app.setReady(true)
slog.Info("readiness enabled")
}()
slog.Info("listening", "addr", ":8080")
if err := http.ListenAndServe(":8080", mux); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("listen failed", "err", err)
}
}What this demonstrates:
PingContext| Probe | Fails when | Orchestrator action |
|---|---|---|
| Liveness | Process deadlocked, cannot serve HTTP at all | Restart pod |
| Readiness | Dependencies down, migrations running | Remove from LB |
| Startup | Still booting past initialDelaySeconds | Wait before liveness |
| Field | Guidance |
|---|---|
periodSeconds | 10s typical for readiness; liveness slightly longer |
timeoutSeconds | Must exceed handler worst case; keep handler under 1s |
failureThreshold | 3 failures common before action |
successThreshold | 1 for readiness after recovery |
// Exempt probes from expensive auth middleware
func skipProbePaths(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/healthz", "/readyz", "/startupz":
next.ServeHTTP(w, r)
return
}
authMiddleware(next).ServeHTTP(w, r)
})
}/healthz - Kubelet does not send your service tokens; probes get 401 and pods restart./healthz from RED dashboards or label probe=true separately.
| Alternative | Use When | Don't Use When |
|---|---|---|
HTTP /healthz | Standard Kubernetes apps | Process has no HTTP server (use exec/tcp) |
| gRPC health protocol | gRPC-only services | Simple REST microservices |
| Exec probe | CLI check scripts | You can expose cheap HTTP instead |
| TCP socket | Listener up is enough | You need dependency validation |
200 for healthy, 503 for not ready.
Avoid 401/403 on probe paths.
Check only hard requirements for serving traffic.
Optional features can degrade without failing readiness if documented.
Sub-millisecond ideally - no I/O, only in-process checks.
It prevents liveness kills while the app loads large caches or runs migrations on first boot.
Yes if you enable readiness before warm-up finishes.
Delay readiness until caches and connection pools are warm.
Optional.
Many teams serve probes on the main HTTP port with dedicated paths.
ALB uses HTTP codes and paths you configure, similar to readiness.
They do not restart instances - they stop routing.
Lightweight Ping with timeout is common.
Heavy queries belong in monitoring, not probes.
Implement grpc.health.v1.Health Check RPC for gRPC-native services.
curl -i localhost:8080/readyz while stopping PostgreSQL and assert 503.
Yes, but orchestrators only care about status codes.
Keep bodies small for human debugging.
None required.
Avoid requiring custom headers kubelet cannot configure easily.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026