What you'll learn
- Why an uptime check has to run from outside your own server
- What a /health endpoint should test, and how to prove it can fail
- The three signals worth watching first: up, slow, erroring
- How false alarms train you to miss real outages, and how to avoid setting that trap
- When a platform's status page saves you from debugging the wrong thing
on this page
Your first app ships with a monitoring system you never set up: a user, DMing you “hey is the site down?” It costs nothing, and during waking hours the coverage is honestly not bad. Everything else about it is wrong. It only fires after someone already hit the broken thing, and it goes quiet at exactly the hours when an outage can stretch from minutes into a whole night unnoticed.
There’s also the part where it stings. Someone liked your project enough to use it, and the role you handed them was smoke detector.
This guide is the minimum viable upgrade. No dashboards yet, no metrics pipeline, none of the heavy tooling. One outside checker, one honest health endpoint, and alerts tuned so you still believe them, which turns out to be the hard part.
Why checks from inside lie
The first idea everyone has is to make the server watch itself. A cron job on the box that curls localhost and emails you if something looks off. Feels sensible, costs nothing.
Walk through what it can actually catch. If your app process crashes while the machine survives, the cron job notices. That’s roughly the whole list. If the entire machine goes down, the cron job goes down with it, and you get silence, which looks identical to good news. Same story if your host has an outage, if your domain’s DNS breaks, if the TLS certificate expires, or if a firewall change quietly blocks port 443. From where your users sit, the site is gone. From inside the box, everything reads fine, because the watcher lives inside the blast radius of nearly every failure worth knowing about.
A check you can trust has to take the same path your users take: a DNS lookup on your real domain, a TLS handshake, an HTTP request over the public internet, and a response that comes back within a reasonable time. Anything less is the server grading its own homework.
The minimum viable upgrade: an outside uptime check
This is the job uptime monitors exist for. UptimeRobot is the classic free one; Better Stack and StatusCake play the same game. The core service is identical across all of them: a server somewhere else on the internet requests your URL every minute or every few minutes, and when the request fails or times out, you get an email or a push notification.
Setup is genuinely a five minute job: create an account, paste a URL, pick where alerts go. Two details are worth getting right on day one, though.
First, monitor your real domain, not the platform’s temporary URL. yourapp.vercel.app can be perfectly healthy while yourapp.com is broken, because your custom domain adds DNS records and a certificate that the temp URL never touches (the custom domains and DNS guide covers why those layers fail independently). The check should exercise every layer a user does.
Second, don’t point it at the homepage. Point it at a path built for the job, which is the next section.
A health endpoint that tells the truth
The lazy uptime check watches your homepage and calls it a day. Here’s the failure mode that slips straight past it: your app process is up, but its database is down.
This happens more than you’d think. The database server restarts, the connection pool dies, you hit the connection limit on your provider’s free tier, a credential rotates. Meanwhile your homepage might be a static file, or cached by a CDN, or a page that renders without touching the database at all. The monitor sees a 200 and stays green. Users can load the homepage, and that’s the full list of things they can do. Login fails, saving fails, every page with real data on it fails.
The fix is a route whose only job is answering one question: can this app do its actual work right now? Convention names it /health (you’ll also see /healthz, a Google-ism that stuck). It returns 200 only when the app can reach what it depends on, and the cheapest way to prove the database is alive is a query that does nothing:
// health.js, an Express route. Adjust the db import to however
// your app talks to its database; the shape is what matters.
import { Router } from "express";
import { db } from "./db.js";
const router = Router();
router.get("/health", async (_req, res) => {
try {
// The cheapest query that proves the connection works:
// no real table, no real rows, near zero cost.
await db.query("SELECT 1");
res.status(200).json({ status: "ok" });
} catch (err) {
console.error("health check failed:", err.message);
// 503 Service Unavailable: the app is up but can't do its job.
// Uptime monitors treat any non-2xx as down, which is the point.
res.status(503).json({ status: "degraded" });
}
});
export default router;
Two design rules keep this from backfiring. Keep it cheap: at one check a minute this route runs 1,440 times a day, so SELECT 1 is the right weight and a query that scans a real table is not. And keep the response boring: this URL is public, so it gets a status word and nothing else. No version numbers, no dependency list, no error details. The gory details belong in your logs.
Before you point a monitor at it, prove the endpoint can actually go red. A health check you’ve never watched fail is a guess:
curl -i http://localhost:3000/health HTTP/1.1 200 OK
content-type: application/json
{"status":"ok"} The good path: your app answered, and the database answered through it.
docker compose stop db Take the database down on purpose. Locally, to be clear.
curl -i http://localhost:3000/health HTTP/1.1 503 Service Unavailable
content-type: application/json
{"status":"degraded"} This is the result that matters. If this still says 200, the health check is lying, and so will any monitor watching it.
docker compose start db Bring it back before you forget you did this.
Deploy that route, then give the uptime monitor https://yourapp.com/health as its target. Now one request per minute exercises DNS, the certificate, the host, your app, and your database, end to end, from outside.
The three signals worth watching
Monitoring as a discipline is bottomless: distributed tracing, log aggregation, dashboards with more graphs than a cockpit. For a first deployment, three signals cover what matters, and they happen to be the three things a user would complain about.
Is it up. The uptime check above. Done.
How slow is it. Most uptime monitors record the response time of every check and chart it for free, so you get this signal without extra work. Ignore the absolute number at first and watch the trend. A health endpoint that answered in 200 milliseconds for two weeks and takes two seconds today is telling you something is wrong, even though nothing is technically down.
How often does it error. The share of requests ending in a 5xx status. Early on, eyeballing your hosting platform’s built-in dashboard or logs once in a while is enough. Proper error tracking, where every crash arrives with a stack trace and a count, deserves its own tool and gets the next guide in this series.
Notice what’s missing: CPU, memory, disk. Nobody has ever DMed you to report that your CPU hit 80 percent. People notice the page failing to load, the page being slow, or the page erroring out, which is exactly the three signals above. Machine metrics are for diagnosis once a signal goes bad: they answer “why” after a symptom has told you “what”. As alarms they’re lousy, because they fire on plenty of states no user would ever feel.
Alert fatigue is the real failure mode
Here’s how monitoring actually fails in practice, and it isn’t the tool breaking.
You set up an alert that’s slightly too sensitive. It fires one night for a blip that resolved itself before you even unlocked your phone. Annoying, whatever. It fires again the next week. By the fourth false alarm you’ve developed a reflex: see that alert name, swipe it away. The reflex doesn’t check whether this one is real; that’s what makes it a reflex. Six weeks later the site goes down for real, the alert fires, and you swipe it away half asleep. The monitoring worked perfectly. You had simply been trained, one false alarm at a time, to ignore it.
The scarce resource here is your own willingness to take an alert seriously, and every false alarm spends some of it. A few rules protect the balance:
- Require consecutive failures. A single failed check on the open internet means very little; a routing hiccup or one slow response can fail a probe. Every decent uptime tool can wait for two or three consecutive failures, or confirm from a second region, before alerting. With one minute checks, that trades two or three minutes of detection time for the disappearance of most false alarms. Take that trade.
- Alert on symptoms, never on causes. “Site has been down for three minutes” deserves to interrupt your evening. “CPU at 80 percent” does not. A busy CPU serving fast pages is a computer doing its job.
- Every alert needs an action. If the honest response to an alert is “it clears itself, ignore it,” delete that alert. Ignoring an alert on purpose is rehearsal for ignoring one by accident.
- Split the channels. Things that mean “users are hurting right now” (down, error spike) go to the loud channel that reaches your phone. Things that mean “look at this sometime this week” (response time creeping up, a certificate expiring soon) go to email. The loud channel staying nearly silent is what keeps it loud.
Check their status page before debugging yours
Status pages matter twice, once in each direction.
Yours first: the same uptime tools will generate a public status page from the checks you already configured, free, on a shareable URL. Turn it on and put the link in your footer. The next time something breaks, a curious user finds “investigating” on that page and goes back to their day. The DM-based monitoring system from the top of this guide is now fully retired: a robot tells you, and a page tells them.
Then theirs: your app stands on other people’s platforms. A host, a database provider, maybe a separate DNS or CDN layer. Each one publishes a status page, and during an outage, theirs is the first place to look, before you touch your own code. The tell is correlation. If the site went down and you didn’t deploy anything, and especially if two unrelated things broke at the same moment, the odds that it’s your bug drop fast. Search “vercel status” or “github status” or whatever you’re standing on. Two minutes of reading can save you an hour of rolling back a deploy that was never the problem.
One caveat from the school of hard knocks: during a large incident, a provider’s status page sometimes stays green for a while after things break, because updating it is a human process. If the page says fine but your feed is full of people yelling about the same provider, trust the yelling.
The whole setup is one evening
In order, for the project you most recently deployed:
- Add
/health, make it touch the database, and break it locally once so you’ve seen it fail. - Deploy it.
- Sign up for a free uptime monitor and point it at
https://yourdomain.com/health. - Set the alert to fire after two or three consecutive failures, into a channel that actually reaches your phone.
- Turn on the public status page and link it from your footer.
- Bookmark your host’s and database provider’s status pages now, while you’re calm and nothing is on fire.
Most of that evening is spent waiting on a deploy.
The difference this makes to shipping is mostly psychological, in a good way. A side project with no monitoring is a thing you check on nervously, like a stove you might have left on. The same project with an outside check on a truthful health endpoint is a thing you can leave alone, build the next feature for, and hand to strangers. The next time it breaks, and it will break, you’ll know at minute two instead of hour six, and the first DM you get can be answered with “yeah, it was down for four minutes this morning, already fixed.” You knew before they did. That’s the entire job.
signal check
Can you answer these without scrolling back up?
-
Your app process is running and the homepage loads, but users say nothing works. Why can a homepage-only uptime check stay green through this?
The homepage can be a static or cached page that never touches the database, so it returns 200 while logins, saves, and every data-backed page fail. A /health route that runs a trivial query like SELECT 1 fails the moment the database does, which is why the monitor should watch /health instead of the homepage.
-
Why set an uptime monitor to alert only after two or three consecutive failed checks?
Single failed checks happen on the open internet without any real outage, and every false alarm trains you to dismiss alerts without reading them. Waiting for consecutive failures costs a couple of minutes of detection time and removes most false alarms, which keeps the alert believable when it finally fires for real.
-
Your site broke and you haven't deployed anything in days. What's the first thing to check?
The status pages of the platforms you run on: your host, your database provider, your DNS or CDN layer. When something breaks with no change on your end, especially several things at once, a provider outage is the likely cause, and confirming it takes a minute instead of an hour spent debugging your own code.
progress is stored in this browser only