Liveness and Readiness Health Checks for Backend Services
Health endpoints look simple, but their semantics directly affect how a production platform routes traffic and restarts applications. A poorly designed check can turn a temporary database slowdown into a restart loop or send requests to an instance that has not finished initializing.
The most useful model separates two questions:
- Liveness: Is this process still capable of running?
- Readiness: Should this instance receive new traffic right now?
Those questions sound similar, but they should usually have different answers and different failure behavior.
Liveness and readiness solve different problems
A liveness endpoint is primarily a process-level signal. If it fails repeatedly, an orchestrator may restart the process. For that reason, liveness should be conservative: only fail when restarting the process is likely to help.
A readiness endpoint is a traffic-routing signal. When it fails, the instance can remain alive while load balancers or orchestrators stop sending new requests to it.
Consider a service whose database is temporarily unavailable. The process itself may be perfectly healthy. Restarting ten application instances will not repair the database. In that situation, readiness can fail while liveness continues to succeed.
A common endpoint convention is:
GET /livez
GET /readyzThe exact paths matter less than keeping their semantics distinct.
Keep liveness deliberately simple
A useful liveness handler often needs no dependency checks at all:
func (h *Health) Live(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}Returning 204 No Content is enough to communicate success without generating an unnecessary response body.
Avoid making liveness depend on a database, cache, message broker, DNS lookup, or third-party API unless failure of that dependency truly means the process itself must be restarted.
Otherwise, a shared dependency outage can cause every application instance to fail liveness simultaneously. The resulting restart storm consumes resources while doing nothing to fix the dependency.
Readiness can represent startup state
Applications often need time to initialize configuration, establish internal state, warm caches, or complete startup migrations before they can safely serve traffic.
A readiness flag provides an explicit gate:
package main
import (
"context"
"net/http"
"sync/atomic"
"time"
)
type Checker func(context.Context) error
type Health struct {
ready atomic.Bool
checks []Checker
}
func NewHealth(checks ...Checker) *Health {
return &Health{checks: checks}
}
func (h *Health) Live(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
func (h *Health) Ready(w http.ResponseWriter, r *http.Request) {
if !h.ready.Load() {
http.Error(w, "not ready", http.StatusServiceUnavailable)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
defer cancel()
for _, check := range h.checks {
if err := check(ctx); err != nil {
http.Error(w, "not ready", http.StatusServiceUnavailable)
return
}
}
w.WriteHeader(http.StatusNoContent)
}After startup work succeeds, the application can mark itself ready:
health := NewHealth(databaseCheck)
// Complete required initialization first.
health.ready.Store(true)atomic.Bool makes the flag safe to read and update concurrently without adding a mutex solely for one boolean value.
Put strict time limits on dependency checks
Readiness checks run repeatedly. A dependency check that can block indefinitely is therefore dangerous.
The example creates a 500ms context timeout before executing its checks. A database check should honor that context:
func databaseCheck(db *sql.DB) Checker {
return func(ctx context.Context) error {
return db.PingContext(ctx)
}
}The correct timeout depends on the service and environment. The important property is that the health endpoint has a predictable upper bound.
Health checks should also be cheap. Avoid expensive queries, large allocations, full table scans, or operations that mutate application data. A readiness endpoint may be called far more frequently than normal monitoring dashboards.
Register the endpoints separately
With Go’s standard http.ServeMux:
mux := http.NewServeMux()
mux.HandleFunc("GET /livez", health.Live)
mux.HandleFunc("GET /readyz", health.Ready)Method-aware ServeMux patterns such as GET /livez require Go 1.22 or newer. On older Go versions, register /livez and /readyz and validate the HTTP method inside the handler if method restriction is required.
Readiness during graceful shutdown
Readiness is useful at the end of the process lifecycle too.
When a service receives a termination signal, it can mark itself unready before shutting down its HTTP server:
health.ready.Store(false)This tells the surrounding platform that the instance should stop receiving new traffic. The application can then begin graceful shutdown and allow existing requests to finish.
There may be propagation delay between changing readiness and upstream load balancers actually removing the instance. Account for that behavior in the deployment platform rather than assuming traffic stops instantaneously.
Which dependencies belong in readiness?
Not every dependency should automatically become a readiness check.
Ask whether the service can provide useful responses when that dependency is unavailable.
For example, a read-only endpoint might continue serving cached data while a background analytics system is offline. Making analytics part of readiness would unnecessarily remove a useful instance from service.
A dependency is a stronger readiness candidate when all or nearly all normal requests require it and the application cannot degrade safely without it.
Avoid leaking internal details
Health endpoints are operational interfaces, not debugging pages. Returning raw database errors, hostnames, connection strings, stack traces, or dependency topology can expose information that clients do not need.
A public response can remain intentionally small:
HTTP/1.1 503 Service Unavailable
not readyDetailed failure information belongs in structured logs, metrics, or traces with appropriate access controls.
Common pitfalls
Using one endpoint for both signals
A single /health endpoint often accumulates contradictory responsibilities. A database outage may then trigger process restarts when the desired behavior was only to stop routing traffic.
Separate liveness and readiness so each signal has one operational meaning.
Checking external services from liveness
This couples process survival to infrastructure outside the process. Shared outages can create synchronized restart loops.
Running expensive readiness queries
Health checks should establish availability, not validate every business invariant. Keep them small and bounded.
Returning success before initialization completes
An HTTP listener can be open while the application is still loading required state. Readiness should remain false until the instance can safely handle normal traffic.
Forgetting shutdown transitions
An instance that remains ready while draining may continue receiving requests at the same time it is trying to terminate.
Treating every optional dependency as critical
Overly strict readiness reduces availability. Include dependencies according to actual request requirements and degradation strategy.
Test the semantics, not only the handlers
At minimum, verify these states:
- Liveness succeeds while the process is running.
- Readiness fails before initialization finishes.
- Readiness succeeds after initialization when critical dependencies are available.
- Readiness fails when a critical dependency check fails or times out.
- Readiness fails after shutdown begins.
These tests protect the operational contract of the service, which is more important than the small amount of handler code itself.
Practical checklist
A production health-check design should answer these questions clearly:
- Does liveness fail only when restarting the process is useful?
- Does readiness represent whether new traffic can be served safely?
- Are dependency checks cheap and bounded by timeouts?
- Can optional dependency failures degrade gracefully instead of removing the instance?
- Does readiness stay false during startup?
- Does readiness become false when graceful shutdown starts?
- Are responses free of credentials and unnecessary infrastructure details?
- Are health endpoints observable through metrics and logs without depending on those systems to succeed?
Health endpoints are small pieces of code with large operational consequences. Keeping liveness focused on process health and readiness focused on traffic eligibility gives deployment systems much better signals—and makes failures considerably easier to manage.