20.7. Health Checks
Table of Contents
Why Health Checks Matter
When your backend is running in production, you need an automated way to answer two questions at any moment:
- Is the application alive?
- Is the application healthy enough to serve real traffic?
Health checks are the mechanism that answers these questions for load balancers, orchestrators like Kubernetes, uptime monitors, and your own dashboards.
A backend that responds to requests is not always healthy.
Never assume “it returns 200” means “everything is fine”.
In this chapter we focus on how to design, implement, and use health checks in a backend system, not on the general topics of logging or monitoring that are covered elsewhere.
Basic Health Check Types
Most systems expose at least two distinct kinds of checks:
| Check type | Typical name / path | Question answered |
|---|---|---|
| Liveness | /live, /healthz | Is the process running at all? |
| Readiness | /ready, /readiness | Can this instance handle real traffic? |
Sometimes you may also see:
| Check type | Typical name / path | Question answered |
|---|---|---|
| Startup | /startup | Has the app fully started and initialized? |
| Deep / full | /health, /deep-health | Are key dependencies healthy, like DB and Redis? |
Liveness Checks
A liveness check answers: “Should this process be killed and restarted?”
It must be:
- Very fast
- Very simple
- Very reliable
A typical liveness check only verifies that:
- The web server loop is running.
- The application can execute simple in-memory logic.
- Optionally, critical internal components are not obviously broken.
Example in words:
- If the liveness endpoint returns a success status, the orchestrator keeps the container/pod alive.
- If it fails repeatedly, the orchestrator restarts it.
What you usually do not check in liveness:
- Database availability.
- External APIs.
- Message queues.
Those belong in readiness or deeper health checks.
Rule: Liveness checks should almost never depend on external systems.
If a downstream dependency is down, you want to degrade or fail requests, not kill the process.
Readiness Checks
A readiness check answers: “Can this instance safely receive production traffic right now?”
This endpoint can be slower and more complex. It typically checks:
- Database connectivity (can you open a connection and run a simple query?).
- Cache connectivity (e.g. ping Redis).
- Message broker availability (e.g. ping RabbitMQ or Kafka).
- Application-level conditions (e.g. background workers warmed up, migrations completed).
If readiness fails:
- The orchestrator or load balancer removes the instance from rotation, but usually does not kill it.
- The instance can recover and become ready again without a restart.
Example scenarios that make an instance “not ready”:
- Database is temporarily overloaded or unreachable.
- App is starting up and still loading a big model or cache.
- App is draining connections before a rolling deployment.
Designing Health Check Endpoints
Well designed health checks share a few common properties.
Clear and Stable URLs
Pick simple, conventional paths and keep them stable:
- Liveness:
/liveor/healthz - Readiness:
/readyor/readiness - Deep health:
/health
Avoid long or “clever” URLs like /check_if_everything_is_ok_now. Many tools assume simple paths.
HTTP Status Codes and Format
Use HTTP status codes to signal result:
- Healthy:
200 OK - Unhealthy:
503 Service Unavailableis common - Starting up / not ready yet: Also often
503for readiness
The body format should be:
- Machine readable (JSON).
- Simple and consistent.
Example JSON response:
{
"status": "ok",
"details": {
"database": "ok",
"redis": "ok",
"queue": "degraded"
}
}
Rule: Health checks must use HTTP status codes correctly.
Do not return 200 OK for a failing health check with an "error" message in the body.
Minimal Authentication
Many systems make health endpoints accessible without auth from:
- Internal load balancers.
- Orchestrators (Kubernetes).
- Uptime monitoring services.
Options:
- No auth but restricted by IP or network (e.g. only internal VPC).
- Simple token in a header:
X-Health-Token: <secret>. - Mutual TLS for sensitive environments.
Be careful not to expose sensitive implementation details in public health endpoints.
Lightweight Implementation
Health endpoints must not become a performance problem.
Guidelines:
- Keep liveness extremely cheap (often just return
200immediately). - Cache results of expensive checks for a short time (for example 5 to 30 seconds).
- Avoid long blocking calls.
Example strategy for readiness:
- Perform expensive checks at an interval in a background task.
- Store the latest result in memory.
- Health endpoint returns that stored result instantly.
Application-Level vs Dependency-Level Checks
A good health check distinguishes between:
- The status of the application itself.
- The status of its dependencies.
Application-Level Health
Application-level checks include:
- The main event loop is running.
- Configuration loaded successfully.
- Critical background tasks are alive.
- No fatal errors that would prevent handling requests.
For example, a simple check:
- Try to allocate a small object.
- Confirm a shared in-memory cache is accessible.
- Verify that the current deployment version is loaded.
Dependency-Level Health
Common dependencies to check:
| Dependency type | Typical check |
|---|---|
| Database | Connect and run SELECT 1 |
| Redis | PING command |
| Message broker | List queues or ping broker |
| Third-party API | Simple “ping” or trivial business request |
The result can be represented like:
{
"status": "degraded",
"details": {
"app": "ok",
"database": "down",
"redis": "ok",
"payment_provider": "timeout"
}
}How you interpret this:
- Liveness might still be
"ok"if the app process is fine. - Readiness might be
"false"because database is"down".
Rule: Separate “can the process run?” from “can the app fulfill its purpose?”.
Liveness is about the process. Readiness is about usefulness.
Example Health Check Designs
Below are high level examples you can adapt to any backend framework.
Minimal Liveness Endpoint
Pseudocode:
@app.get("/live")
def live():
return {"status": "ok"}This only checks that the HTTP server and app are responding.
Readiness with Dependency Checks
Pseudocode with simple checks:
@app.get("/ready")
def ready():
checks = {}
# Database
try:
db.execute("SELECT 1")
checks["database"] = "ok"
except Exception:
checks["database"] = "down"
# Redis
try:
redis_client.ping()
checks["redis"] = "ok"
except Exception:
checks["redis"] = "down"
overall = "ok"
if "down" in checks.values():
overall = "unhealthy"
status_code = 200 if overall == "ok" else 503
return JSONResponse(
status_code=status_code,
content={"status": overall, "details": checks}
)Typical behavior:
- If both DB and Redis are up, returns
200. - If either is down, returns
503and the failing component is visible indetails.
Startup Health Check
During startup, the app may need to:
- Run database migrations.
- Load configuration or secrets.
- Warm up caches.
Example idea:
- Expose
/startupthat only returns200once all initialization is complete. - Kubernetes and similar systems can use this to delay routing traffic until startup succeeds.
Pseudocode:
startup_done = False
@app.on_event("startup")
def on_startup():
global startup_done
# initialization code here
startup_done = True
@app.get("/startup")
def startup():
if startup_done:
return {"status": "ok"}
raise HTTPException(status_code=503, detail="Starting up")Integrating Health Checks with Infrastructure
Health checks become powerful when integrated with other components.
Load Balancers
Load balancers (like Nginx, HAProxy, AWS ALB) can:
- Periodically call your health endpoint.
- Remove instances from rotation if checks fail.
- Add them back when checks pass again.
Example configuration idea:
- Use
/readyfor traffic decisions. - Use relatively short timeouts and intervals, such as:
- Interval: 5 to 10 seconds.
- Healthy threshold: 2 successful checks.
- Unhealthy threshold: 2 to 3 failed checks.
Container Orchestration (Kubernetes Example)
Kubernetes uses different probes:
| Probe | Typical endpoint | Purpose |
|---|---|---|
| liveness | /live | Restart pod if probe fails |
| readiness | /ready | Send or stop sending traffic |
| startup | /startup | Allow long startups gracefully |
Descriptive behavior:
- If liveness fails repeatedly, Kubernetes kills and restarts the pod.
- If readiness fails, the pod stays running but is removed from Service endpoints.
- If startup probe fails, Kubernetes will restart the pod, but until it passes, liveness and readiness are ignored.
Uptime Monitoring Tools
External tools like:
- UptimeRobot
- Pingdom
- StatusCake
will periodically call a health or homepage URL.
Best practice:
- Expose a public friendly endpoint like
/healththat: - Returns
200only when you want to show “service is up” on your status page. - Might be less detailed than internal health checks.
If your app is partially degraded but not fully down, you can still decide to return 200 and show a “degraded performance” status at the application or business level.
Health Checks and Graceful Shutdown
During deployments or maintenance your app should:
- Stop accepting new traffic.
- Finish in-flight requests.
- Shut down cleanly.
Health checks help coordinate this.
A typical sequence:
- Mark the instance as not ready:
- Readiness endpoint starts returning
503or"ready": false. - Load balancer removes the instance from rotation.
- Instance continues to handle current in-flight requests until they finish.
- After a timeout, the process exits.
Example behavior for a shutdown hook:
- During shutdown, set a flag so
/readyreturns unhealthy. - Keep
/livereturning healthy until very close to final termination.
This avoids:
- Dropped requests during rolling deployments.
- Restart loops caused by health checks failing too early.
Health Check Best Practices
To close, here is a compact list of practical rules you can follow when designing and implementing health checks.
Health Check Rules:
- Separate liveness and readiness.
Liveness answers “should I restart?” Readiness answers “should I send traffic?” - Keep liveness cheap and internal.
No external calls, minimal logic, very fast. - Check dependencies in readiness or deep health.
Database, cache, and external APIs belong here. - Use proper HTTP status codes.
200 OKfor healthy,503 Service Unavailablefor unhealthy. - Make responses machine readable and simple.
Use JSON with a clearstatusand optionaldetails. - Do not expose sensitive info.
Avoid secrets, internal hostnames, or stack traces in public health endpoints. - Cache expensive checks when necessary.
Use short-lived in-memory caching to reduce load. - Integrate with infrastructure.
Configure load balancers, orchestrators, and uptime monitors to use these endpoints. - Test failure scenarios.
Simulate DB down, slow dependencies, or partial outages and confirm health behavior. - Document your health endpoints.
Other teams and tools should know which paths to call and how to interpret responses.
By following these practices, your backend becomes more observable, resilient, and friendly to automated systems that keep it running reliably in production.
Views: 7
KAHIBARO