← Knowledge hub
Tutorialby Uptime Room· September 24, 2026· 6 min read

Health Check Endpoint Design: What It Should Return

A health check endpoint answers one question for one caller. Here is how to pick the caller, choose 200 or 503, and keep the wrong things out of the body.

A health check endpoint is a route whose only job is to answer one question for one caller. The design work is deciding which caller you mean, because an orchestrator, a load balancer and an outside probe want three different answers. Get that wrong and the endpoint reports healthy while your users cannot buy anything.

Three callers, three different questions

Before choosing a path or a payload, name who is asking. The Health Check API pattern treats this as one endpoint, but in practice the three callers pull in opposite directions.

CallerReal questionShould touch dependenciesFailure means
Liveness probeIs this process wedged?NoRestart the container
Readiness probeCan this instance serve now?Yes, the ones it needs to serveRemove it from the pool
Outside probeCan a user reach this at all?Whatever the public path needsPage someone

Liveness is the one people over-engineer. A liveness route that queries the database will restart a perfectly healthy process during a database blip, which turns a slow dependency into a rolling outage. Kubernetes documents this trap directly and recommends keeping liveness cheap.

What should a health check endpoint return?

A status word, a list of the checks that ran, and how long each took. That is enough for a human reading it at 3am and for a machine parsing it. Anything more is a debug page wearing the wrong name.

{
  "status": "degraded",
  "checks": [
    { "name": "primary-db", "status": "up", "latency_ms": 4 },
    { "name": "cache", "status": "down", "latency_ms": 2001, "error": "timeout" },
    { "name": "queue", "status": "up", "latency_ms": 11 }
  ]
}

Three states beat two. Up means everything required is working, down means the instance cannot serve, and degraded means it can serve with something missing. The cache above is a good example: losing it makes the service slow rather than broken, so the instance should stay in the pool and someone should still find out.

Keep the latency numbers. They turn the endpoint into a first diagnostic rather than a yes or no, and a dependency drifting from 4ms to 400ms is visible weeks before it starts failing.

Shallow or deep?

A shallow check proves the process is running and serving HTTP. A deep check reaches into the database, the cache and the broker. Both are legitimate, and the mistake is exposing only one of them.

Deep checks have a failure mode worth respecting. If twelve services all deep-check the same database, one slow database marks all twelve unready at once, and the pool empties even though every instance could have served cached reads. The pattern write-up lists this cascade among the drawbacks of the approach.

The rule we prefer is simple. Check a dependency only if the instance genuinely cannot serve its main job without it. Everything else belongs in the body as degraded rather than in the status code.

Deep checks also cost something on every call. Cache the verdict for a few seconds and serve the cached copy. Otherwise a fleet of probes at a one-second cadence becomes real load on the dependency you are testing.

200 or 503?

Use the status code, and make sure it agrees with the body. A healthy instance returns 200. An instance that cannot serve returns 503 Service Unavailable, which is the code load balancers and orchestrators already understand. Degraded stays at 200, because the instance can still take traffic.

The silent failure here is real and common. An endpoint that returns 200 with "status":"down" inside the body will be read as healthy by every load balancer on earth, because nothing in the path parses your JSON. Azure's health endpoint monitoring guidance makes the same point about relying on response codes rather than payload text.

Set a hard timeout inside the handler too. A health check endpoint that hangs for thirty seconds is worse than one that fails, because the caller waits instead of acting. Two seconds is plenty, and whatever did not answer in time is reported as down.

What does not belong in it

Most of what gets added over time. The endpoint is usually unauthenticated, publicly routable, and scraped by anything that finds it.

Two things leak information you would rather keep.

  • Secrets and connection strings. Report primary-db: up, never the hostname or the user it connected with.
  • Version and commit detail. Build metadata on a public route tells an attacker which advisories to read.

Three more simply produce wrong answers.

  • Third-party APIs you do not control. Your instance is not unhealthy because a payment provider is slow.
  • Unbounded fan-out. One health call should not trigger forty downstream calls. Check what you own, once.
  • Authentication the prober cannot satisfy. Restrict by network or an unguessable path instead.

Metrics are the other thing people try to smuggle in. Memory percentages and queue depth belong on a metrics route, because they are numbers to graph rather than a verdict to act on. The distinction is the same one behind the four golden signals.

Watching it from outside the cluster

The third caller is the one framework guides forget. An orchestrator checks the endpoint from inside the network, so it stays happy while DNS, TLS or the load balancer in front of it is broken. Only a probe from outside walks the same path a user does.

curl -X POST https://uptimeroom.com/api/v1/monitors \
  -H "Authorization: Bearer urm_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "public health",
    "type": "keyword",
    "target": "https://example.com/health",
    "keyword": "\"status\":\"up\"",
    "intervalSec": 60,
    "regions": ["london", "sao-paulo"],
    "timeoutMs": 5000
  }'

Matching on the body rather than the code is deliberate. It is the one arrangement that catches an endpoint returning 200 while reporting itself down. We tested this against a deliberately mismatched endpoint. The keyword check failed within one cycle, while a plain status check stayed green.

Run it from two of the six probe regions available as of September 2026. The honest limit is worth stating plainly. An outside probe reads whatever the endpoint chooses to report, so an endpoint that lies still looks fine. Egress addresses for allowlisting are published on the probe IPs page.

Common mistakes

One endpoint for all three callers. Serving liveness, readiness and the public check from a single route means one of them gets the wrong answer. Two routes is usually the right number, plus the public one.

Logging every call. A 30 second interval from three callers is thousands of log lines a day that drown the ones that matter. Exclude the path at the access log level.

Never testing the failure path. An endpoint that has only ever returned 200 is untested code. Break a dependency in staging and confirm you actually get a 503, because a try block that swallows the exception is the usual culprit.

Treating it as the whole story. A route can answer correctly while checkout is broken, since nothing in it exercises a real transaction. That is why a health check api sits alongside a synthetic user journey rather than replacing one, as covered in synthetic monitoring.

Design it backwards from the caller and the rest falls out. Cheap and dependency-free for liveness, honest about required dependencies for readiness, and a status code that matches the body for everyone. Then point something outside your own network at it, because a health check endpoint nobody watches from the user's side is a log line rather than an alert. Two regions and a one-minute interval are in the free plan, which is enough to prove the whole path works.

FAQ

What is a health check endpoint?

It is an HTTP route whose only job is to report whether an instance can do its work. A liveness caller asks whether the process is wedged, a readiness caller asks whether this instance can serve traffic now, and an outside probe asks whether a user can reach the service at all. Those three want different answers.

What is the healthz endpoint?

The healthz name is a convention popularised by Google and carried into Kubernetes, where a trailing z marks an operational route rather than an application one. It usually means the cheap liveness check: no database, no cache, just proof that the process is running and serving HTTP. The name matters far less than the semantics behind it.

Should a health check return 200 or 503?

Return 200 when the instance can serve and 503 Service Unavailable when it cannot. Load balancers and orchestrators act on the status code and do not parse your JSON, so a 200 carrying a down status in the body is read as healthy. Degraded states stay at 200, because the instance can still take traffic.

What is an HTTP health check?

It is a caller sending a plain HTTP request to a known route and deciding from the response whether the target is usable. Load balancers use it to choose which instances receive traffic, orchestrators use it to restart or hold back pods, and external monitors use it to detect an outage the whole way through DNS, TLS and the network path.

Should a health check endpoint require authentication?

Usually not, because anything that cannot authenticate cannot check it, and load balancers rarely can. Keep it unauthenticated but boring: a status word, the checks that ran and their timings. Restrict it by network or an unguessable path if you must, and never put secrets, connection strings or build metadata in the response.

Related reading