Health checks are essential in distributed systems. They help Kubernetes and load balancers understand the state of your service. But not all health checks serve the same purpose.
This post explains the difference between liveness and readiness probes, why they exist, and how to implement them correctly in real microservices.
Liveness Probe
A liveness probe answers one question:
Is the application still running, or should Kubernetes restart it?
Key points:
- Designed to detect deadlocks, stuck event loops, memory corruption, or unrecoverable internal states.
- A failing liveness probe results in a container restart.
- Should be lightweight and must not depend on external systems (database, cache, message broker, etc.).
- If your liveness probe depends on a database and the database has an outage, your pod will get stuck in a restart loop — which is exactly what you do not want.
A simple 200 OK is usually enough.
Readiness Probe
A readiness probe answers a different question:
Is this service ready to receive traffic right now?
Key points:
- If a readiness probe fails, Kubernetes keeps the pod running but removes it from the load balancer.
- Safe to include dependency checks: database, Redis, message broker, external API, cache warmup, etc.
- Useful during startup, warm-up, graceful shutdown, or when dependencies temporarily fail.
This ensures your service only receives traffic when it can actually handle it.
HTTP-Based Implementation
The common pattern is to expose two endpoints:
-
/livez→ basic check to confirm the application is alive -
/readyz→ deeper check to confirm the service is ready
Below are examples in Go and Node.js.
Go Example
Liveness:
func Livez(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
Readiness with database and Redis checks:
type Dependencies struct {
DB *sql.DB
Redis *redis.Client
}
func (d *Dependencies) Readyz(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := d.DB.PingContext(ctx); err != nil {
http.Error(w, "db not ready", http.StatusServiceUnavailable)
return
}
if err := d.Redis.Ping(ctx).Err(); err != nil {
http.Error(w, "redis not ready", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("READY"))
}
Kubernetes configuration:
livenessProbe:
httpGet:
path: /livez
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
Node.js Example (Express)
Liveness:
app.get("/livez", (req, res) => {
res.status(200).send("OK");
});
Readiness with DB + Redis:
app.get("/readyz", async (req, res) => {
try {
await db.ping();
await redis.ping();
res.status(200).send("READY");
} catch (err) {
res.status(503).send("NOT READY");
}
});
gRPC Health Checks
Kubernetes supports gRPC probes via grpc.health.v1.Health/Check.
However, this API only exposes a single health state, making it difficult to cleanly separate liveness from readiness.
Because of this limitation, many real-world teams expose HTTP-based /livez and /readyz endpoints, even when the service itself is gRPC-only.
This approach is simple, flexible, and aligns well with Kubernetes.
Real-World Examples
If you want to see real world implementations, check out:
go-microservice-boilerplate
Includes ready-to-use/livezand/readyzendpoints.kind-microservices-demo
A demo microservices setup, with KIND and Docker Compose environments.
You can use these as reference implementations or a starting point for your own projects.
Conclusion
Liveness and readiness probes are simple concepts, but misusing them can lead to cascading failures, stuck restarts, or pods receiving traffic during startup.
A good rule of thumb:
- Liveness is about process health.
- Readiness is about dependency and traffic readiness.
Separate them cleanly, keep liveness lightweight, and let readiness reflect the true runtime health of your system.
Top comments (0)