Shipping ItLesson 25 of 74

Contents

Shipping It

Lesson 25 of 74. Sign in to keep your place.

Module 3 · Knowing what it is doing

The health check that lies

9 min read

The check that proves nothing

JavaScript
app.get("/health", (req, res) => res.json({ ok: true }));

This tells you that the process is running and the HTTP layer works. It will return 200 while the database is unreachable, the disk is full, the cache is down and every real request is failing.

It is not useless (a load balancer needs something cheap to poll) but it must not be the thing you trust when someone asks whether the site is up.

Liveness and readiness are different questions

  • Liveness: should I be restarted? Answer no unless the process is wedged: a deadlock, an exhausted event loop. Keep it trivial.
  • Readiness: should I receive traffic right now? Answer no while you are still warming up, while migrations are running, or while your database connection is broken.

Getting them the wrong way round is a classic and it is spectacular. Put a database check in your liveness probe and a database blip restarts every container at once, which drops every connection, which makes the database worse, which fails the next probe. A restart loop across your whole fleet, caused by a monitoring decision.

Rule: liveness restarts, readiness routes. Only readiness gets to depend on anything outside the process.

A deep check that does not become the problem

A useful readiness check touches the dependencies you cannot work without:

JavaScript
app.get("/ready", async (req, res) => {
  const checks = await Promise.allSettled([
    withTimeout(db.query("select 1"), 800),
    withTimeout(cache.ping(), 300),
  ]);
  const ok = checks.every((c) => c.status === "fulfilled");
  res.status(ok ? 200 : 503).json({ ok, checks: summarise(checks) });
});

Two details that are not optional. Timeouts on every dependency, or the check hangs instead of failing and the platform learns nothing. And cache the result for a few seconds, because a probe every second across ten instances means ten extra database queries a second forever, and when the database is struggling, the health checks pile on and finish it off. A health check that makes an overloaded system worse is a feedback loop, not a diagnostic.

Return 503, not 200-with-a-false-field. Infrastructure reads the status code.

Your own monitoring runs on the machine that is down

This is the sentence to remember. If your health check is polled from inside the same box, the same cluster, or the same region, it goes silent exactly when you need it, and silence is not an alert.

So you need at least one check that runs somewhere else entirely, on infrastructure you do not control, from more than one region.

The free options are real:

  • UptimeRobot: 50 monitors on the free plan, five-minute interval.
  • Uptime Kuma: open source, self-hosted, and if you host it on a different provider from your app it counts as outside.
  • Better Stack and Cronitor both have usable free tiers.
  • A GitHub Actions cron job that curls your site and opens an issue on failure. Crude, free, works.

Monitor the thing that matters, not the homepage

A homepage is often static and cached, so it stays up while the application is dead. Check something that exercises the real path: log in as a test user and load a page that queries the database. That is a synthetic check, and one of them is worth twenty pings.

While you are there, monitor the things from the module ahead of you too: TLS certificate expiry and domain expiry are one HTTP check each and they prevent two of the most embarrassing outages there are.

Say 503 during a deploy

When you are shutting down, stop accepting new requests, finish the ones in flight, and answer readiness with 503 and a Retry-After. That is how a load balancer learns to stop sending you traffic before you disappear, which is the difference between a clean deploy and a handful of failed requests every time.

The signal handler is short and almost nobody writes it:

JavaScript
process.on("SIGTERM", async () => {
  ready = false;             // readiness now returns 503
  await sleep(5000);         // let the load balancer notice
  server.close(() => process.exit(0));
});

Those five seconds of doing nothing are the most valuable five seconds in your deploy.

Every lesson here is open. Your progress is recorded once you pass the Agents and Automation exam.

Check your understanding

A brief database hiccup causes every container in your fleet to restart simultaneously, and the restart loop continues after the database recovers. Which configuration mistake produces exactly this?

Pick the one you would defend. Your answer stays private.

Next lesson · 8 minAlerts you would get out of bed for

Stuck on something?Ask Tri, your AI tutor

How to cite: Addaly (2026) ‘The health check that lies’, in Shipping It. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/cloud-and-shipping/health-checks-that-tell-the-truth (Accessed: 13 October 2026).