tutorial

API Health Check Best Practices in 2026

A health check endpoint is one of the most important pieces of infrastructure in any API service — and one of the most commonly implemented wrong. Done well,...

A health check endpoint is one of the most important pieces of infrastructure in any API service — and one of the most commonly implemented wrong.

Done well, a /health endpoint gives your monitoring tools, orchestrators, and load balancers an accurate picture of your service's state in milliseconds. Done poorly, it either masks real failures by always returning 200, or triggers false alerts by surfacing transient issues as critical failures.

This guide covers what to include in a health check endpoint, standardised response formats, the difference between liveness and readiness, dependency checks, response time targets, and how to wire it into external monitoring with Vigilmon.


What to Include in a Health Check Endpoint

A minimal health check that just returns 200 OK is better than nothing. But a well-designed health check endpoint communicates significantly more:

1. Overall status — is the service able to handle traffic right now?

2. Dependency status — which downstream dependencies are healthy, degraded, or down?

3. Version and build info — useful for verifying the right version is deployed.

4. Response time of dependency checks — how long did it take to query the database or cache?

5. Uptime — how long has the service been running since last restart?


Standardised JSON Response Schema

The IETF has a draft standard for health check responses (RFC draft: Health Check Response Format), and it's worth following. A well-structured health response looks like this:

{
  "status": "pass",
  "version": "1.4.2",
  "releaseId": "20260615.1",
  "uptime": 86400,
  "checks": {
    "database:responseTime": [
      {
        "status": "pass",
        "time": 4,
        "unit": "ms"
      }
    ],
    "cache:responseTime": [
      {
        "status": "pass",
        "time": 1,
        "unit": "ms"
      }
    ],
    "storage:diskSpace": [
      {
        "status": "warn",
        "output": "Disk usage at 78%",
        "unit": "percent"
      }
    ]
  }
}

Status values:

  • pass — healthy
  • warn — degraded but operational
  • fail — unhealthy, should not receive traffic

HTTP status codes to match:

  • pass200 OK
  • warn200 OK (or 207 Multi-Status)
  • fail503 Service Unavailable

Return 503 when your service is genuinely unable to serve traffic. This tells load balancers and orchestrators to stop routing to this instance.


Liveness vs. Readiness Checks

Kubernetes popularised the distinction between liveness and readiness probes, and it applies beyond Kubernetes to any service with complex startup or dependency requirements.

Liveness Check

Question it answers: Is the process alive and not deadlocked?

A liveness check should be extremely fast and check nothing external. It's purely about whether the process is running and responding. If the liveness check fails, the orchestrator restarts the process.

GET /health/live
→ 200 OK
{"status": "pass"}

Liveness checks should never check the database, cache, or any external service. If your database is down, you want your service to stay running (and return errors or serve cached data) — not be killed and restarted repeatedly.

Readiness Check

Question it answers: Is the service ready to receive traffic?

A readiness check verifies that all required dependencies are reachable and the service has completed startup. If readiness fails, the load balancer stops routing traffic to this instance without restarting it.

GET /health/ready
→ 200 OK or 503
{"status": "pass", "checks": {...}}

Readiness checks should include dependency verification. A service that can't reach its database shouldn't receive traffic.


Dependency Checks

For each critical dependency, your health check should verify connectivity and measure response time. Keep these checks lightweight — you're not running a full integration test, you're doing a quick ping.

Database: Run a simple query (SELECT 1 or equivalent). Measure the round-trip time.

Cache (Redis): Use PING. Expect PONG back in under 1ms on a healthy instance.

Message queue: Check if the broker connection is alive and the consumer is attached.

External APIs: Only include if they're hard dependencies — services you can't function without. Include circuit breaker state if you're using one.


Code Examples

Node.js (Express)

const express = require('express');
const app = express();

app.get('/health/ready', async (req, res) => {
  const checks = {};
  let overall = 'pass';

  // Database check
  const dbStart = Date.now();
  try {
    await db.query('SELECT 1');
    checks['database:responseTime'] = [{ status: 'pass', time: Date.now() - dbStart, unit: 'ms' }];
  } catch (err) {
    checks['database:responseTime'] = [{ status: 'fail', output: err.message }];
    overall = 'fail';
  }

  // Cache check
  const cacheStart = Date.now();
  try {
    await redis.ping();
    checks['cache:responseTime'] = [{ status: 'pass', time: Date.now() - cacheStart, unit: 'ms' }];
  } catch (err) {
    checks['cache:responseTime'] = [{ status: 'warn', output: err.message }];
    if (overall === 'pass') overall = 'warn';
  }

  const statusCode = overall === 'fail' ? 503 : 200;
  res.status(statusCode).json({ status: overall, checks, version: process.env.APP_VERSION });
});

Python (FastAPI)

from fastapi import FastAPI
from fastapi.responses import JSONResponse
import time

app = FastAPI()

@app.get("/health/ready")
async def readiness():
    checks = {}
    overall = "pass"

    # Database check
    start = time.time()
    try:
        await db.execute("SELECT 1")
        checks["database:responseTime"] = [{"status": "pass", "time": round((time.time() - start) * 1000), "unit": "ms"}]
    except Exception as e:
        checks["database:responseTime"] = [{"status": "fail", "output": str(e)}]
        overall = "fail"

    status_code = 503 if overall == "fail" else 200
    return JSONResponse(status_code=status_code, content={"status": overall, "checks": checks})

Go

func healthReadyHandler(w http.ResponseWriter, r *http.Request) {
    checks := map[string]interface{}{}
    overall := "pass"

    // Database check
    start := time.Now()
    if err := db.PingContext(r.Context()); err != nil {
        checks["database:responseTime"] = []map[string]interface{}{
            {"status": "fail", "output": err.Error()},
        }
        overall = "fail"
    } else {
        checks["database:responseTime"] = []map[string]interface{}{
            {"status": "pass", "time": time.Since(start).Milliseconds(), "unit": "ms"},
        }
    }

    statusCode := http.StatusOK
    if overall == "fail" {
        statusCode = http.StatusServiceUnavailable
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(statusCode)
    json.NewEncoder(w).Encode(map[string]interface{}{
        "status": overall,
        "checks": checks,
    })
}

Response Time Targets

Your health check endpoint should respond fast. If the health check itself is slow, orchestrators may timeout and incorrectly mark the service as unhealthy.

| Check type | Target response time | |-----------|---------------------| | Liveness | < 5ms | | Readiness (no dependencies) | < 10ms | | Readiness (with DB check) | < 100ms | | Readiness (with DB + cache) | < 150ms |

If your readiness check takes more than 200ms to respond under normal conditions, that's a signal that your dependency checks themselves are slow — worth investigating.

Use timeouts in your dependency checks rather than letting them block indefinitely:

const dbResult = await Promise.race([
  db.query('SELECT 1'),
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 2000))
]);

Setting Up Vigilmon API Monitoring

Once your health check endpoint is live, external monitoring with Vigilmon provides a second layer of verification from outside your infrastructure.

Key Vigilmon settings for API health monitoring:

  1. Monitor your /health/ready endpoint — not just / or your main API path
  2. Set check interval to 1 minute — catches issues within 60 seconds
  3. Assert on HTTP status code — expect 200, alert on 503
  4. Optional: assert on response body — check that "status":"pass" appears in the response
  5. Enable multi-region consensus — Vigilmon's default ensures alerts only fire when your endpoint is genuinely down, not when a single probe has a bad moment

Vigilmon's multi-region consensus is particularly valuable for API monitoring because APIs are more likely than static sites to have region-specific routing issues. Getting a consensus-confirmed failure means the problem is real, not a network blip between one probe and your load balancer.


Common Mistakes to Avoid

Returning 200 from a /health endpoint unconditionally. A health check that never returns 503 is useless for orchestration. If your service can't reach its database, it should say so.

Including slow checks in liveness probes. Liveness checks that query the database will cause cascading restarts when the database is slow. Keep liveness checks process-local only.

Not setting timeouts on dependency checks. A database that stops responding will hang your health check indefinitely, making your service appear healthy to anything that never receives a response.

Exposing sensitive internal data. Health check endpoints are often publicly accessible. Don't include internal IPs, connection strings, or secrets in the response output.


Conclusion

A well-designed health check endpoint is a contract between your service and its infrastructure. It tells load balancers when to route traffic, tells orchestrators when to restart instances, and tells external monitors when something is genuinely wrong.

The key decisions: separate liveness from readiness, check dependencies in readiness probes (with timeouts), return 503 when the service is unhealthy, and follow a consistent response schema.

External monitoring with Vigilmon gives you a verification layer that's independent of your infrastructure — confirming your API is reachable from the outside world, not just internally healthy.

Start monitoring your API endpoints free: vigilmon.online

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →