Cloud Networking in Production · Names, Load Balancers and Timeouts

The Health Check That Took Everything Down

Health checks that test shared dependencies mark every instance unhealthy at once. Keep load balancer checks shallow and about the instance.

Create a free account to save this page, take notes and track your progress. Sign in

Scenario

Example team: to make health checks "meaningful", an API's /health endpoint queries the database, calls the payment provider and checks Redis. The load balancer and Kubernetes liveness probes both use it.

What you see in production

The payment provider has a 40-second slowdown. Every instance's /health times out at once. Kubernetes restarts all pods (liveness failures), the load balancer removes every target, and the whole API is down for 6 minutes, including the 90% of endpoints that don't use payments. Restarted pods hammer the database with cold caches and connection storms as they come back.

The naive code

@app.get("/health")
def health():
    db.execute("SELECT 1")
    payments.ping()             # third-party dependency
    redis.ping()
    return {"ok": True}
# used for: ALB target health check AND Kubernetes livenessProbe

Reproduce it

A dependency failure is shared by every instance, so a health check that includes it fails everywhere at the same moment:

Deep versus shallow health checks during a dependency outageTry it yourself
instances = [f"api-{i}" for i in range(1, 9)]
dependency_ok = False                     # the payment provider is slow
instance_ok = {name: name != "api-3" for name in instances}   # api-3 really is broken (out of memory)

def deep_check(name):
    return instance_ok[name] and dependency_ok

def shallow_check(name):
    return instance_ok[name]

for label, check in [("deep", deep_check), ("shallow", shallow_check)]:
    healthy = [n for n in instances if check(n)]
    print(f"{label:7} check: {len(healthy)}/{len(instances)} healthy -> ", end="")
    if not healthy:
        print("no targets: outage (or the load balancer fails open to all targets)")
    else:
        print(f"api-3 removed, others keep serving; payment calls degrade gracefully")

Output

deep    check: 0/8 healthy -> no targets: outage (or the load balancer fails open to all targets)
shallow check: 7/8 healthy -> api-3 removed, others keep serving; payment calls degrade gracefully

Root cause

A health check should answer "should this instance get traffic?" Including shared dependencies turns it into "is the whole system healthy?", whose answer is the same for every instance. A partial outage of one dependency becomes a full outage, and liveness probes multiply it by restarting healthy processes. (An AWS ALB fails open when every target is unhealthy and routes to all of them anyway, which can hide or worsen the problem depending on the cause.)

The fix

Separate the checks by purpose:

Check Answers Includes dependencies? Used by
Liveness Is the process stuck? Restart it. No Kubernetes livenessProbe
Readiness / LB health Can this instance serve requests now? Only local ones (warmed up, own connection pool not exhausted) Load balancer, readinessProbe
Dependency monitoring Is the database or payment provider healthy? Yes Dashboards, alerts, circuit breakers
@app.get("/livez")
def livez():
    return {"ok": True}                       # the event loop answers: the process is alive

@app.get("/readyz")
def readyz():
    if not app.state.warmed_up or app.state.db_pool.exhausted():
        return JSONResponse({"ready": False}, status_code=503)
    return {"ready": True}

Handle dependency failures in the request path instead: timeouts, circuit breakers and graceful degradation ("payments temporarily unavailable" while browsing still works).

Trade-offs

  • Shallow checks can keep an instance in rotation while one of its dependencies is broken for it alone (for example a bad local network path); cover that with request error-rate alarms and outlier detection.
  • Liveness probes that are too aggressive restart slow-but-healthy processes; give them generous thresholds.

Detect it in production

  • Alert when the healthy-host count drops by more than a few instances at once: that's almost always a shared cause.
  • Track health-check latency; checks that call dependencies slow down before they fail.
  • Review health endpoints in code review: any network call in a liveness probe is a red flag.

Key points

  • Health checks decide traffic per instance; shared dependencies make every instance fail together.
  • Liveness: process only. Readiness/LB: local ability to serve. Dependencies: monitoring and circuit breakers.
  • An ALB fails open when all targets are unhealthy; don't design around it, avoid the situation.

Practice questions

Why shouldn't a load balancer health check call the database or third-party APIs?

Answer

Those dependencies are shared, so when one fails every instance fails its health check at once and the load balancer removes them all, turning a partial outage into a full one.

What's the difference between liveness and readiness probes?

Answer

Liveness asks whether the process should be restarted (keep it trivial); readiness asks whether the instance should receive traffic now (warm-up, local resources). Neither should depend on shared external services.

What does an AWS Application Load Balancer do when all targets are unhealthy?

Answer

It fails open and routes requests to all registered targets, on the assumption that the health checks themselves are wrong.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?