Exhibit A
Probe a worker's readiness endpoint
http://10.0.2.17:8080/readyzGET 200 in 240 ms(simulated)
Request
3 headersResponse
200 OKResponse: 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_statusfield (whether a number may send).
How does a health check work?
Keep liveness shallow
/livezreturns 200 if the process can answer at all. No database, no network calls, so a dependency blip never triggers a restart storm.Make readiness test hard local dependencies
/readyzchecks what this instance needs to do useful work: aSELECT 1on its database pool, a ping to its queue, config and secrets loaded. Give each check a short timeout.Report soft dependencies without failing
Include the Graph API, CRM or AI gateway as informational fields (
degraded, latency) withaffects_readiness: false. Circuit breakers, not health checks, handle their failures.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.
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 200passes 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
/healthfor 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.