Health Check Passes but the App Is Down: How to Monitor a Container for Real

The container says running, the port answers, the health check passes, and every request a person makes returns 500. Three of the four ways an app breaks look exactly like this.

Four ways for an app to be down, probed in a real container: three of the four broken states were reported healthy by the container status, the port check and the health check, and only the crashed process was caught, while a health check that touches the database reports unhealthy and 503 for the same broken app

The way most people find out their app is down is that somebody tells them. Not a monitor, not an alert — a message, usually hours later, often from the one person who bothered to mention it.

That is not because monitoring is hard. It is because the monitors an app ships with answer a question nobody asked: is the process running? The question that matters is whether a person can use the thing.

So we ran one app in four different states of broken, in a real container, and asked four monitors what they saw.

Watch it instead

Keeping the app alive is number 10 in our countdown of what actually goes wrong for people building with AI.

Video: We Analyzed 130,000 Vibe Coder Posts. These Are the Problems That Actually Hurt.
Watch it instead We Analyzed 130,000 Vibe Coder Posts. These Are the Problems That Actually Hurt.
From the Back4app channel. This guide is the long version of its tenth position; the countdown itself is in the companion post. Open on YouTube →

What did the monitors actually report?

This, and it is worse than it sounds:

Four ways to be down and what each monitor reported: three of four broken states show the container up, the port up and the health check healthy, while a user gets 500 or a timeout

Read the second row slowly. The container is running. The port answers. The health check returns 200. The container’s own health status is healthy. And every request a person makes comes back 500.

Nothing in that row is lying, exactly. Each monitor is answering its own narrow question correctly. It is just that none of those questions is the one you need answered.

How we measured: one Node app with three switchable failure modes, built into an image and run under docker run with a --health-cmd, then probed with docker inspect, nc and curl. Stable across two consecutive runs — the lab is in the repo.

Why does the ordinary health check pass?

Because of what is in it. This is the endpoint almost every generated app ships with, and it is not wrong so much as empty:

if (req.url === '/health') return send(200, { status: 'ok' });

It proves exactly one thing: this process is able to answer an HTTP request. It does not know whether the database is reachable, whether the queue is draining, or whether the thing it was built to do still works.

Each monitor is answering its own narrow question correctly. None of those questions is the one you need answered.

The failures that matter leave the process perfectly able to answer. A database that went away, a third-party API that changed, an endpoint that hangs — in all three the process is fine. It is the app that is not.

What should the health check do instead?

Touch the thing the app cannot work without, and fail when that thing is gone:

if (req.url === '/healthz') {
  const up = await dependencyUp();          // actually opens a connection
  return send(up ? 200 : 503, { status: up ? 'ok' : 'dependency unreachable' });
}

We changed nothing else — same image, same broken app, same probes:

StateHealth checkContainer healthA user request
working normally200healthy200
database unreachable, /health200healthy500
database unreachable, /healthz503unhealthy500

The app was equally broken in the last two rows. The difference is entirely whether anything was willing to say so.

What about the failure that hangs?

That one is nastier, and our third row shows why. The process answers /health in a millisecond while the endpoint people actually use never returns at all.

A health check cannot catch this, because by construction the health check is the cheap path and the hang is on the expensive one. What catches it is measuring the request a person makes.

So the alert worth having is not “is it up”. It is a real request to a real endpoint, on a timeout, from outside your infrastructure — the same thing a user does, done on a schedule. Anything else is checking that the lights are on.

Where do the errors go?

Wherever you sent them, and this decides whether you can diagnose anything afterwards.

Our failing request printed one line to stdout, and we read it back out of the platform after the fact:

{"level":"error","msg":"orders failed: dependency unreachable"}

That worked because it went to stdout. Had it gone to a log file inside the container, it would have been discarded the next time that container was replaced — which is every deploy, every scale event, every host migration.

What does a platform take off your list?

The part that is maintenance rather than judgement. Server updates, database patches and the infrastructure underneath are somebody else’s work, which removes the slow version of this problem — the app that degrades for months because nothing was ever updated.

Collected logs matter more than they sound, too. A dashboard that keeps what your app printed means the evidence from last Tuesday still exists, instead of having left with a container.

What no platform can do is write your health check. Only your code knows what your app cannot work without, and that is precisely the thing the check has to touch.

What should you do first?

Open your app and look at what /health returns. If it is a constant, you do not have a health check — you have a line that says 200.

Make it open a connection to your database and return 503 when it cannot. That is usually five lines, and it converts the most common outage you will ever have from invisible to alerting.

Then add one check from outside that requests a real page on a timeout. Between those two, the three failures in our table that reported healthy stop being able to.

Tagsmonitoringhealth-checkuptimedockerlogsmaintenance

Frequently asked questions

Why does my health check pass while the app is broken?

Because the usual health endpoint only proves the process can answer. We ran one app in four states and probed each: with a database it could not reach, the container reported running, the port answered, the health check returned 200 — and a real request returned 500. The check never touched the thing that was broken.

What should a health check actually do?

Exercise the dependencies the app cannot work without, and return a failing status when they are unreachable. We swapped the endpoint for one that opens a connection to the database before answering: the same broken app then reported 503, and the container's own health status flipped from healthy to unhealthy.

Why does my app go down without anyone noticing?

Because the three most common failures are invisible to the monitors that come as standard. A dependency going away, an endpoint that hangs and a slow memory leak all leave the process running and the port open. Only a crash changes what those monitors see, and a crash is the failure you would have found out about anyway.

Where do the errors go when an app fails in a container?

To wherever you wrote them. Anything printed to stdout or stderr is captured by the platform and retrievable afterwards — we read the failing request back out of docker logs. Anything written to a log file inside the container is discarded when that container is replaced, which happens on every deploy.

Does a managed platform remove this problem?

It removes the maintenance half: server updates, database patches and infrastructure are operated for you, and logs are collected and kept rather than living inside a container that is about to be replaced. What stays yours is the health check, because only your code knows what your app cannot work without.

Which backend as a service provides the most reliable uptime guarantees?

An uptime guarantee covers the platform, not your app. In this post three of four broken states of the same app reported healthy on every monitor it came with, and only the crashed process was caught. Swapping the health check for one that touches the database turned the broken app into unhealthy · 503, which is what lets any platform restart it or alert you.

Further reading

Reviewed & verified by Back4app Engineering, Editorial review at Back4app

Tested Oct 27, 2026 — Local: Docker 29.4.3 on macOS, arm64 · App: Node 22 alpine, one container · Probes: docker inspect, nc, curl · Backend: Back4app, free plan.

Changelog

  • Oct 31, 2026 — The companion repository is not published yet, so the link to it and the button were removed; they come back when it is.
  • Oct 28, 2026 — The companion repository is published, so the button and the inline link to it are back.

More from Back4app Engineering

Containers · error-guide

Why Your App Works Locally and Crashes When You Deploy It [Full Repo]

Containers · tutorial

How to Run a Node.js Bot, Webhook or Cron Job 24/7 Without a VPS [Full Repo]

Web Deployment · tutorial

How to Undo Changes an AI Coding Assistant Made: What Git Recovers, What the Platform Recovers, What Is Gone