CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl-labs/health-checks

Health check and readiness endpoints for web services — liveness probes,

97

3.61x
Quality

99%

Does it follow best practices?

Impact

94%

3.61x

Average score across 4 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

SKILL.mdskills/health-checks/

name:
health-checks
description:
Health check and readiness endpoints for web services — liveness probes, dependency checks, and graceful startup/shutdown. Use when building any backend service that will be deployed behind a load balancer, in containers, or in any environment that monitors service health.
keywords:
health check, readiness probe, liveness probe, kubernetes health, load balancer health, dependency check, graceful shutdown, service health, health endpoint, ready endpoint
license:
MIT

Health Checks

Every deployed service needs health endpoints. Load balancers, container orchestrators, and monitoring tools depend on them.


Two Endpoints

EndpointPurposeChecksReturns
GET /healthLiveness — is the process running?Nothing (always 200 if reachable){"status": "ok"}
GET /readyReadiness — can it serve requests?Database connection, dependencies{"status": "ok"} or {"status": "degraded", "checks": {...}}

Why two?

  • A service can be alive (process running) but not ready (database down)
  • Load balancers should stop sending traffic to unready services
  • Container orchestrators restart services that fail liveness

Implementation

Node.js (Express)

app.get("/health", (_req, res) => {
  res.json({ status: "ok", timestamp: new Date().toISOString() });
});

app.get("/ready", (_req, res) => {
  const checks: Record<string, string> = {};

  // Check database
  try {
    db.prepare("SELECT 1").get();
    checks.database = "ok";
  } catch {
    checks.database = "failed";
  }

  const allOk = Object.values(checks).every(v => v === "ok");
  res.status(allOk ? 200 : 503).json({
    status: allOk ? "ok" : "degraded",
    checks,
    timestamp: new Date().toISOString(),
  });
});

Python (FastAPI)

@app.get("/health")
async def health():
    return {"status": "ok", "timestamp": datetime.utcnow().isoformat()}

@app.get("/ready")
async def ready():
    checks = {}
    try:
        db.execute("SELECT 1")
        checks["database"] = "ok"
    except Exception:
        checks["database"] = "failed"

    all_ok = all(v == "ok" for v in checks.values())
    status_code = 200 if all_ok else 503
    return JSONResponse(
        status_code=status_code,
        content={"status": "ok" if all_ok else "degraded", "checks": checks}
    )

Go

mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
    writeJSON(w, 200, map[string]string{"status": "ok"})
})

mux.HandleFunc("GET /ready", func(w http.ResponseWriter, r *http.Request) {
    checks := map[string]string{}
    if err := db.Ping(); err != nil {
        checks["database"] = "failed"
    } else {
        checks["database"] = "ok"
    }
    allOk := true
    for _, v := range checks { if v != "ok" { allOk = false } }
    status := 200
    if !allOk { status = 503 }
    writeJSON(w, status, map[string]any{"status": map[bool]string{true: "ok", false: "degraded"}[allOk], "checks": checks})
})

Graceful Startup

Readiness must return 503 until the service is fully initialized — database connected, caches warmed, migrations applied. Register the readiness route early but gate it:

let isReady = false;

app.get("/ready", (_req, res) => {
  if (!isReady) return res.status(503).json({ status: "starting" });
  // ... normal dependency checks
});

// After all initialization completes:
await db.connect();
await runMigrations();
isReady = true;

Without this, the load balancer sends traffic before the service can handle it.


Kubernetes / Container Configuration

Probe Configuration

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10
  failureThreshold: 3       # restart after 3 consecutive failures
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
  failureThreshold: 3       # remove from service after 3 failures
startupProbe:               # for slow-starting services
  httpGet:
    path: /health
    port: 8080
  failureThreshold: 30
  periodSeconds: 2           # up to 60s to start

Dockerfile HEALTHCHECK

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

Important Details

  • No authentication on health endpoints — monitoring tools need unauthenticated access
  • Don't log health check requests — they fire every few seconds, they'll drown your logs
  • Keep checks fast — health endpoints should respond in <100ms; set a timeout so a hung dependency doesn't block the probe
  • 503 for not ready — this is the standard "service unavailable" status
  • Don't check external services in liveness — only in readiness. If the database is down, the service is alive but not ready. Checking deps in liveness causes cascading restarts across the cluster when a shared dependency fails.
  • Shallow vs deep checks — liveness is always shallow (just "am I alive?"). Readiness can do deep checks (DB ping, Redis ping, queue connection) but keep them bounded by a timeout.

Checklist

  • GET /health returns 200 with {"status": "ok"}
  • GET /ready checks database and returns 503 if down
  • Health endpoints excluded from authentication
  • Health endpoints excluded from request logging
  • Health endpoints excluded from rate limiting
  • Readiness returns 503 during startup until fully initialized
  • Liveness does NOT check external dependencies
  • Kubernetes probes configured (or Dockerfile HEALTHCHECK for non-k8s deployments)

Verifiers

  • health-endpoint — Add /health and /ready endpoints for liveness and readiness probes

skills

health-checks

tile.json