Skip to content

Health Check

Also called: Health check endpoint · Liveness probe · Readiness probe · Healthz

Definition
An endpoint or probe that tells a load balancer or orchestrator whether an instance is alive and ready for traffic, so it can restart or route around it.

Exhibit A

Probe a worker's readiness endpoint

2 / 2
GEThttp://10.0.2.17:8080/readyz

GET 200 in 240 ms

Request

3 headers
GET /readyz HTTP/1.1
Host: 10.0.2.17:8080
User-Agent: kube-probe/1.31
Accept: application/json

Response

200 OK
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"status": "ready",
"checks": {
"postgres": {
"status": "up",
"latency_ms": 3
},
"redis_queue": {
"status": "up",
"latency_ms": 1
},
"graph_api": {
"status": "up",
"affects_readiness": false
}
}
}
Try another response
Press Send to probe one worker's /readyz the way Kubernetes does. Toggle the alternatives: a lost database, a draining pod, and a slow Graph API that is reported but does not fail readiness.

Response: 200 OK after 240 ms.

What is a health check?

A health check is a lightweight endpoint, such as GET /livez or GET /readyz, or a probe command, that infrastructure calls every few seconds to ask one instance whether it is working. Load balancing depends on the answer, since balancers send traffic only to instances that pass; orchestrators such as Kubernetes use it to restart stuck containers and to hold traffic back from instances that are still starting or are shutting down.

There are three common kinds. A liveness check asks whether the process is alive and not deadlocked; failing it gets the container restarted, so it should test almost nothing beyond the process itself. A readiness check asks whether the instance can serve requests right now: is it connected to its database and queue, are its config and secrets loaded? Failing it takes the instance out of rotation without killing it. A startup check gives slow-booting services time before liveness checks begin.

The key judgement is which dependencies count. Hard, local dependencies (the instance's own database connection, its queue) belong in readiness. Shared external dependencies, above all Meta's Graph API, do not: if every instance marks itself unready because the Cloud API is slow, the load balancer has nowhere to send traffic, your webhook receiver starts returning errors and Meta's retries pile up. Report the Graph API's state as informational and let a circuit breaker and the queue absorb the slowness.

"Health" means three different things on a WhatsApp stack. Your health checks probe your own instances. Synthetic monitoring sends a real test message through the whole journey from outside. The Health Status API returns Meta's health_status for a phone number, WABA or template, whose can_send_message value (AVAILABLE, LIMITED or BLOCKED) says whether it may send. And a Flow endpoint receives Meta's own health check, a request with action: "ping" that it must answer with a status of active.

Key takeaways

  • A health check is an endpoint or probe that reports whether one instance can do its job, so infrastructure can restart it or stop sending it traffic.
  • Liveness answers "is the process stuck?" (restart it); readiness answers "can it take traffic now?" (route around it).
  • Readiness should test hard local dependencies such as the database and queue, never Meta's API: a slow Graph API would empty the whole fleet at once.
  • Return 200 when healthy and 503 when not, with no caching; Kubernetes treats any status from 200 to 399 as a pass.
  • It probes your own instances, unlike synthetic monitoring (the journey from outside) or Meta's health_status field (whether a number may send).

How does a health check work?

  1. Keep liveness shallow

    /livez returns 200 if the process can answer at all. No database, no network calls, so a dependency blip never triggers a restart storm.

  2. Make readiness test hard local dependencies

    /readyz checks what this instance needs to do useful work: a SELECT 1 on its database pool, a ping to its queue, config and secrets loaded. Give each check a short timeout.

  3. Report soft dependencies without failing

    Include the Graph API, CRM or AI gateway as informational fields (degraded, latency) with affects_readiness: false. Circuit breakers, not health checks, handle their failures.

  4. Wire probes with sensible timing

    Set the interval, timeout and failure threshold for each probe, and a startup probe for slow boots. Readiness can be quick to fail; liveness should be patient.

  5. Fail readiness first when shutting down

    On SIGTERM, mark the instance as draining so readiness returns 503 and the load balancer stops sending new requests, finish in-flight work, then exit.

A health check in code

let draining = false;
process.on("SIGTERM", () => {
draining = true; // readiness fails first...
setTimeout(() => server.close(), 15_000); // ...then finish in-flight work and exit
});
// Liveness: the process answers. No dependency calls.
app.get("/livez", (_req, res) => res.status(200).send("ok"));
// Readiness: can THIS instance do useful work right now?
app.get("/readyz", async (_req, res) => {
const checks = {
postgres: await probe(() => db.query("SELECT 1")),
redis_queue: await probe(() => redis.ping()),
// Informational only: a slow Graph API must not take every instance out of rotation.
graph_api: { status: graphBreaker.isOpen() ? "degraded" : "up", affects_readiness: false },
};
const ready = !draining && checks.postgres.status === "up" && checks.redis_queue.status === "up";
res
.set("Cache-Control", "no-store")
.status(ready ? 200 : 503)
.json({ status: draining ? "draining" : ready ? "ready" : "not_ready", checks });
});
async function probe(fn, timeoutMs = 500) {
const started = Date.now();
try {
await Promise.race([fn(), new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), timeoutMs))]);
return { status: "up", latency_ms: Date.now() - started };
} catch (err) {
return { status: "down", error: err.message };
}
}

Shallow liveness, readiness limited to hard local dependencies, Meta reported but never decisive, and a drain on shutdown.

Common mistakes with a health check

  • Putting Meta in the readiness check

    A shared upstream that slows down makes every instance unready together, turning a Meta slowdown into your outage. Report it as informational and let breakers and queues handle it.

  • Deep checks in liveness

    If liveness calls the database, a 30-second database failover restarts every container, which then all reconnect at once. Keep liveness to the process itself.

  • A health check that always says 200

    A static return 200 passes even when the database pool is exhausted, so traffic keeps arriving at a broken instance. Test the things the instance actually needs.

  • Expensive probes

    Heavy queries or external calls on every probe, multiplied by every instance every few seconds, add real load. Use trivial queries, short timeouts, and cache results for a second or two if needed.

  • Exposing internals publicly

    Hostnames, versions and error strings help attackers. Serve detailed health on an internal port or network, and give any public endpoint only a status.

Why does a health check matter?

Health checks are what make high availability (HA) automatic. Without them, a load balancer keeps sending webhooks to a crashed instance and a hung worker sits forever holding jobs. Badly designed checks are almost as harmful: a liveness probe that calls the database restarts every container during a database blip, and a readiness probe tied to Meta turns a slow Graph API into a full outage of your own service. A few dozen lines of careful code decide whether failures stay small.

Example: The probe that took down a webhook fleet

A Jaipur travel company's webhook receivers had a readiness probe that sent a test call to the Graph API. One evening Graph API latency rose to about two seconds for twenty minutes. Every probe timed out, every pod went unready, the load balancer returned 503 to Meta, and webhooks backed up for the whole incident, though nothing on the company's side was broken. The fix moved the Graph API check into an informational field, kept readiness to the database and queue, and let a circuit breaker slow sends instead. The next Graph API slowdown caused a little queueing and no dropped webhooks.

Best practices for a health check

  • Use separate paths for liveness and readiness; one /health for both forces bad trade-offs.
  • Log readiness state changes, not every probe, so flapping instances stand out.
  • Include the build version in the internal readiness body to spot a bad rollout quickly.
  • Pair health checks with synthetic monitoring: healthy instances do not prove the journey works.

Health Check FAQs

What is the difference between liveness and readiness probes?+
Liveness decides whether to restart an instance; readiness decides whether to send it traffic. Liveness should fail only when the process is stuck, while readiness fails whenever the instance temporarily cannot serve, such as during startup, shutdown or a lost database connection.
Should a health check call the WhatsApp Cloud API?+
No, not in a check that decides readiness or liveness. A slow Graph API would fail every instance at once; report its state as an informational field and use a circuit breaker for the send path.
What HTTP status should a health check return when unhealthy?+
Return 503 Service Unavailable. Load balancers and Kubernetes treat 200 to 399 as healthy and anything else as a failure, and 503 also tells humans the instance is temporarily unable to serve.
Is a health check the same as Meta's health_status?+
No. Your health check reports whether your own instance works; Meta's `health_status` field, requested on a phone number, WABA or template, reports in `can_send_message` whether it may send (AVAILABLE, LIMITED or BLOCKED). Monitor both, separately.
How often should health checks run?+
Every 5 to 10 seconds is typical. Readiness can run more often with a low threshold so bad instances leave rotation fast, while liveness should tolerate several misses before a restart.

See InfiQ pricing

Try it on the official WhatsApp Business API

InfiQ is an official Meta Business Partner. Start a 7-day free trial — no card needed — and see this running on your own number.