KAHIBARO
Discord Login Register

20.7. Health Checks

Why Health Checks Matter

When your backend is running in production, you need an automated way to answer two questions at any moment:

  1. Is the application alive?
  2. 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 typeTypical name / pathQuestion answered
Liveness/live, /healthzIs the process running at all?
Readiness/ready, /readinessCan this instance handle real traffic?

Sometimes you may also see:

Check typeTypical name / pathQuestion answered
Startup/startupHas the app fully started and initialized?
Deep / full/health, /deep-healthAre key dependencies healthy, like DB and Redis?

Liveness Checks

A liveness check answers: “Should this process be killed and restarted?”

It must be:

A typical liveness check only verifies that:

Example in words:

What you usually do not check in liveness:

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:

If readiness fails:

Example scenarios that make an instance “not ready”:

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:

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:

The body format should be:

Example JSON response:

json
{
  "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:

Options:

Be careful not to expose sensitive implementation details in public health endpoints.

Lightweight Implementation

Health endpoints must not become a performance problem.

Guidelines:

Example strategy for readiness:

Application-Level vs Dependency-Level Checks

A good health check distinguishes between:

Application-Level Health

Application-level checks include:

For example, a simple check:

Dependency-Level Health

Common dependencies to check:

Dependency typeTypical check
DatabaseConnect and run SELECT 1
RedisPING command
Message brokerList queues or ping broker
Third-party APISimple “ping” or trivial business request

The result can be represented like:

json
{
  "status": "degraded",
  "details": {
    "app": "ok",
    "database": "down",
    "redis": "ok",
    "payment_provider": "timeout"
  }
}

How you interpret this:

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:

python
@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:

python
@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:

Startup Health Check

During startup, the app may need to:

Example idea:

Pseudocode:

python
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:

Example configuration idea:

Container Orchestration (Kubernetes Example)

Kubernetes uses different probes:

ProbeTypical endpointPurpose
liveness/liveRestart pod if probe fails
readiness/readySend or stop sending traffic
startup/startupAllow long startups gracefully

Descriptive behavior:

Uptime Monitoring Tools

External tools like:

will periodically call a health or homepage URL.

Best practice:

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:

  1. Stop accepting new traffic.
  2. Finish in-flight requests.
  3. Shut down cleanly.

Health checks help coordinate this.

A typical sequence:

  1. Mark the instance as not ready:
    • Readiness endpoint starts returning 503 or "ready": false.
  2. Load balancer removes the instance from rotation.
  3. Instance continues to handle current in-flight requests until they finish.
  4. After a timeout, the process exits.

Example behavior for a shutdown hook:

This avoids:

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:

  1. Separate liveness and readiness.
    Liveness answers “should I restart?” Readiness answers “should I send traffic?”
  2. Keep liveness cheap and internal.
    No external calls, minimal logic, very fast.
  3. Check dependencies in readiness or deep health.
    Database, cache, and external APIs belong here.
  4. Use proper HTTP status codes.
    200 OK for healthy, 503 Service Unavailable for unhealthy.
  5. Make responses machine readable and simple.
    Use JSON with a clear status and optional details.
  6. Do not expose sensitive info.
    Avoid secrets, internal hostnames, or stack traces in public health endpoints.
  7. Cache expensive checks when necessary.
    Use short-lived in-memory caching to reduce load.
  8. Integrate with infrastructure.
    Configure load balancers, orchestrators, and uptime monitors to use these endpoints.
  9. Test failure scenarios.
    Simulate DB down, slow dependencies, or partial outages and confirm health behavior.
  10. 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

Comments

Please login to add a comment.

Don't have an account? Register now!