DEV Community

Libme
Libme

Posted on

Uptime Monitor Says 100% While Users See Errors: What to Check, and When Paid Monitoring Is Worth It

An uptime monitor that reports 100% during an outage is almost always measuring the wrong thing: a CDN-cached page, a health endpoint that returns 200 whether or not the database answers, or a check that only looks at the status code. The fix is two health endpoints (shallow for the load balancer, deep for the external monitor), Cache-Control: no-store on both, and an assertion on the response body. Paid monitoring is worth it once your customers notice outages before you do — what you are buying is a tighter check interval, multi-region confirmation, and a way to page a human.

Why did the monitor say "up" when the site was down?

The incident that made me rewrite my monitoring: a customer emailed a screenshot of a 500 page while the uptime dashboard showed a flat green line. The app had been throwing ECONNREFUSED on every database call for about forty minutes.

The monitor was hitting https://example.com/, which sat behind a CDN with a long cache TTL. The edge kept serving cached HTML while the origin was on fire: status 200, fast response, green line. The monitor was accurately reporting that the CDN was up.

Three patterns cause almost every "100% uptime during an outage" report:

  1. The checked URL is cached. The CDN answers instead of your origin. curl -sI https://example.com/ | grep -iE 'age|cache|x-cache' will show a non-zero Age or a HIT header if this is happening to you.
  2. The health endpoint is shallow. /health returns {"status":"ok"} as long as the process is alive. Process alive, database gone, still 200.
  3. The check only asserts on the status code. Some frameworks render an error page with a 200. Some maintenance pages do too. A status-code-only check cannot tell the difference between "the app works" and "the app produced HTML."

An uptime check is only as honest as the endpoint it hits, and a cached 200 is the most common lie.

How should a health endpoint be built so a monitor can trust it?

You need two endpoints with different jobs, because the load balancer and the external monitor are asking different questions.

The load balancer asks "should I keep routing to this instance?" That check must be shallow. If it includes the database, one database blip makes every instance fail readiness at the same moment, the load balancer pulls all of them, and a thirty-second hiccup becomes a full outage.

The external monitor asks "can a user actually get served?" That check should be deep: touch the database, touch the cache, and fail loudly with a 503 if any of them do not answer within a tight timeout. Here is the shape I use, in Express with pg and redis (ESM, so top-level await works):

import express from "express";
import pg from "pg";
import { createClient } from "redis";

const app = express();
const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

function withTimeout(promise, ms, name) {
  let timer;
  const timeout = new Promise((_, reject) => {
    timer = setTimeout(
      () => reject(new Error(`${name} timed out after ${ms}ms`)),
      ms
    );
  });
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}

// Shallow: "is this process alive?" — for the load balancer only.
app.get("/health", (req, res) => {
  res.set("Cache-Control", "no-store");
  res.json({ status: "ok" });
});

// Deep: "can this instance serve a real request?" — for the external monitor.
app.get("/health/deep", async (req, res) => {
  res.set("Cache-Control", "no-store");
  const checks = {
    postgres: withTimeout(db.query("SELECT 1"), 2000, "postgres"),
    redis: withTimeout(redis.ping(), 1000, "redis"),
  };

  const results = {};
  let healthy = true;
  for (const [name, check] of Object.entries(checks)) {
    try {
      await check;
      results[name] = "ok";
    } catch (err) {
      results[name] = err.message;
      healthy = false;
    }
  }

  res
    .status(healthy ? 200 : 503)
    .json({ status: healthy ? "ok" : "degraded", checks: results });
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Three details matter more than the rest. The timeouts are short and explicit, because a health check that hangs for thirty seconds on a dead connection pool is worse than one that fails fast. Cache-Control: no-store goes on both endpoints, and you then verify the CDN honors it — some edge rules cache regardless of origin headers, in which case you route /health/* around the cache entirely. And the deep check covers only dependencies you own. Put a third-party API in it and their outage pages you at 3 a.m. for something you cannot fix.

Then configure the monitor to hit /health/deep and assert on the body, not just the code. Every serious uptime tool supports a keyword or JSON match; use it to require "status":"ok" in the response. That single assertion closes the gap where an error page with a 200 slips through.

The load balancer gets the shallow check and the monitor gets the deep one; giving both the same endpoint is how a database blip turns into a total outage.

What are you actually paying for with an uptime monitoring subscription?

Free tiers are generous enough that the paid decision comes down to a handful of capabilities. As of September 2026, pricing models cluster around monitor counts and check intervals, but the numbers move often enough that you should read the current plan page rather than trust any post.

What you pay for Free tier reality Why it matters
Check interval Every few minutes Interval is your worst-case detection delay; a 3-minute outage can fit inside a 5-minute gap
Multi-region confirmation Usually one location One vantage point produces false alarms and misses regional DNS or routing failures
Body / JSON assertions Often limited Without them you are back to status-code-only checks
Browser (synthetic) checks Rare The only way to verify "login and checkout still work," not just "the URL answers"
On-call routing Email only Phone or SMS escalation is what wakes someone up at night
Status page Basic Cuts support email during an incident

The hidden costs are the ones that scale. Per-monitor pricing feels cheap until every microservice and cron job has its own check. SMS and voice alerts are frequently metered separately. Browser checks are typically priced per run, so a Playwright flow every minute from three regions costs meaningfully more than a ping.

The four tools I keep coming back to each own a distinct slice of this problem.

If you just need a free ping every few minutes with email alerts, UptimeRobot is the one most people start with, and for a single side project it is often enough. Its limit is that the free interval is coarse, and assertions and status pages get thin once you need more than the basics.

If you want the managed version of the whole loop, Better Stack is the one that bundles uptime checks with on-call scheduling and a status page so a failed check pages a human without a second tool. The drawback is that you are paying for incident management you may not need yet, and seats plus phone alerting add up for a small team.

If your real question is whether a user can still log in and check out, Checkly is the one that runs Playwright scripts on a schedule from multiple regions and keeps the check definitions as code in your repo. The trade-off is maintenance: browser checks break when your UI changes, and per-run pricing punishes aggressive schedules.

For cron jobs and queue workers that have no URL to hit, Healthchecks.io is the one built around the dead-man's switch: the job pings it on success, and you get alerted when the ping stops arriving. It does not do HTTP monitoring at all, so it complements rather than replaces the others.

Pay for monitoring when the cost of a missed outage exceeds the subscription, and for most small products that line is crossed the first time a customer reports the outage before your monitor does.

When is the free tier genuinely enough?

A free ping monitor is fine when you have one or two public endpoints, a five-minute detection delay is acceptable, email is a reasonable alert channel because nobody is on call anyway, and a deep health endpoint like the one above is what it hits. Solo side projects and internal tools usually qualify.

Move to a paid tier at the first of these signals: paying customers who would churn over an unnoticed outage, a second person who needs to be paged, a purchase flow that can break while the homepage stays up, or an SLA you signed. Fix the endpoint first either way; a paid monitor pointed at a cached homepage costs more and still lies.

The free tier stops being free the day a customer becomes your monitoring system.

FAQ

Why does my uptime monitor show 100% uptime when my site is down?
Because it is checking something other than your application: a CDN-cached page, a shallow health endpoint that returns 200 while the database is unreachable, or a check that only looks at the status code. Point it at a deep health endpoint with Cache-Control: no-store and assert on the response body.

Should a health check endpoint check the database?
Yes for the endpoint your external monitor hits, no for the one your load balancer uses. A deep check with short timeouts tells you users are being served; a shallow liveness check keeps a brief database blip from making the load balancer drop every instance at once.

How often should an uptime monitor check my site?
The check interval is your maximum detection delay. Every five minutes is acceptable for hobby projects; every minute with confirmation from a second region is the baseline once customers depend on the service.

Bottom line

Fix the endpoint before you buy anything: a deep /health/deep with dependency timeouts, no-store headers, and a body assertion in the monitor closes the "100% uptime during an outage" hole on any tool's free tier. Solo projects and internal tools can stay on UptimeRobot's free ping and Healthchecks.io for cron jobs. Teams with paying customers should pay for one-minute multi-region checks and phone escalation, which is where Better Stack fits. If a broken login flow would go unnoticed while the homepage stays green, add Checkly's browser checks for the one or two flows that actually make money.

Related reading

Top comments (0)