Everyone worries about false positives: the monitor says the site is down, you drop everything, and it was fine. The quieter failure is the opposite. The dashboard shows a month of green, and meanwhile the checkout has been returning a styled error page for six hours, or the login redirect has been looping, or the homepage loads and nothing behind it works. You find out from a customer. The monitor did exactly what you told it to; you just told it to check the wrong thing.
This guide lists the seven ways a plain HTTP status check gets fooled, with the specific setting that fixes each. Status and timeout checks are free on CronAlert; keyword, regex, and content-change assertions are Pro ($5/month) and are marked where they come up.
Why a status check is easy to fool
A basic uptime check does one thing: request a URL and compare the status code to the one you expect, usually 200. That is a statement about whether some server sent a successful response, not about whether the response was your site. Anything that answers 200 in your place keeps the monitor green: a login page, a CDN's cached copy, a web server's default page, a parking page at your registrar, a maintenance screen. A check is only as good as the thing it asserts, so every fix below is a way of asserting something that only the working site can produce.
| # | What users see | What the monitor sees | Fix |
|---|---|---|---|
| 1 | Login or Access page instead of the app | 302 followed to a 200 | Keyword, or auth in a custom header |
| 2 | Styled error or maintenance page | 200 | Keyword (contains or not_contains) |
| 3 | Page loads, every button fails | 200 from the static shell | Monitor the API endpoints |
| 4 | Site up, database down | 200 from a health endpoint that checks nothing | Deepen the endpoint, assert its body |
| 5 | Parked domain or old server | 200 from the wrong host | Keyword; monitor every hostname users type |
| 6 | "Welcome to nginx!" | 200 | Keyword |
| 7 | Emails, exports, backups stop | 200 from the website | Heartbeat monitors |
1. The redirect to a login page
This is the most common one, and it bites people who did everything else right. The app lives behind single sign-on, Cloudflare Access, a Tailscale or VPN gateway, or just its own session check. An unauthenticated request gets a 302 to the login page. Uptime monitors follow redirects, exactly as a browser does, so the monitor lands on the login page, the login page says 200, and the check passes. It will keep passing when the app behind the login is throwing exceptions, because the monitor never gets that far. Monitoring Cloudflare Tunnels walks through the Access version of this trap in detail.
Fix A: assert on the destination. Add a keyword assertion (Pro) for text that appears only once you are through: the app's name in a nav bar, a data-app attribute, a JSON field. If the monitor ends up on the login page, that text is missing and the check fails. Alternatively assert not_contains on something only the login page has, such as name="password".
Fix B: get through the door. Put the credential the gateway accepts in the monitor's custom headers: Cloudflare Access service-token headers, a bearer token for an API, a basic-auth header, or a session cookie for a dedicated monitoring user. The custom-headers guide covers each case, including why you cannot ask a monitor to assert on the 302 itself: redirects are followed before the status is compared, so the expected status describes the final page.
If you cannot do either, monitor a URL that is deliberately outside the login wall. Most frameworks let you exempt a /health or /up route; see section 4 for what it should check.
2. The error page that returns 200
A surprising number of error pages are served with a 200. Some frameworks render a friendly "Something went wrong" template and forget to set the status. Hosting panels and managed WordPress platforms show a branded error or maintenance page with a 200 because that is what a static file server returns. A CDN configured to serve stale content on origin errors returns a 200 copy of whatever it had, sometimes for hours. The user sees a broken site; the status check sees success.
Fix: a keyword assertion (Pro). Choose a string that is in the raw HTML of the working page and nowhere else: a footer line, a product name, a specific heading. The monitor reads the body as the server sent it, so pick text that is in the HTML source, not something injected by JavaScript after load. Keyword monitoring explains contains versus not_contains; the not_contains form is handy when you know the error text (Maintenance, Error 500, went wrong) but the success text changes often.
For sites behind a CDN, also check the response time in the check history. A stale-while-error copy is suspiciously fast and uniform, which is a hint that nothing reached your origin. If you want a hard assertion, include a build or deploy identifier in the HTML (a meta tag with the git SHA is enough) and use a content changed monitor (Pro) on a tiny endpoint that returns it; it alerts whenever the served build differs from the last check, which includes the moment a CDN swaps in a cached copy from a previous deploy.
3. The single-page app whose shell loads while its API is dead
A React, Vue, or Svelte front end is a static HTML file plus JavaScript, usually served from a CDN. It will load perfectly when your API, your database, and your auth provider are all down. The user sees a spinner, an empty list, or a toast that says "Failed to fetch"; the monitor on the homepage sees a 200 from a CDN edge and says UP. A monitor on the homepage of an SPA is a monitor on the CDN.
Fix: monitor what the page calls. Open the browser's network tab on the page that matters, note the first two or three requests it makes after load, and create a monitor for each: a session or config endpoint, the main data endpoint, search. Assert on a key in the JSON they return ("status":"ok", "items"). If the request needs a token, store a long-lived one in the monitor's custom headers. API endpoint monitoring has a worked example, and GraphQL endpoints need a slightly different approach because HTTP monitors send no request body.
Keep the homepage monitor too. It tells you when the CDN or DNS is the problem, which the API monitors cannot distinguish.
4. The health endpoint that checks nothing
Someone added /health that returns {"ok":true} as a constant. It is honest about one thing: the process is running and the router works. It says nothing about the database connection pool, the cache, the queue, or the third-party API the product depends on, so the site can be entirely non-functional while /health is green. Pointing an external monitor at a shallow health endpoint converts a real outage into a false negative by design.
Fix: make the endpoint earn its 200. A good external health check runs one cheap query against the database, one PING against the cache, and returns a body that names each component. Then set the monitor's keyword assertion to the all-clear string and let the endpoint return a non-200 when any check fails, so even the free status-only monitor catches it. Designing health check endpoints covers shallow versus deep checks and has code for the common frameworks; database health endpoints goes deeper on the database part.
Do not put slow or expensive work in it. The monitor's timeout is the only slowness alarm an HTTP check has, so a health endpoint that takes eight seconds under load will start alerting as a timeout, which is at least honest but is not the signal you wanted.
5. The wrong host: parked domains, stale DNS, and the apex nobody checks
Users type example.com; you monitor www.example.com. The apex record points at an old server, a registrar's parking page, or nothing, and the www monitor stays green. Or the domain expired, the registrar's holding page returns a cheerful 200 with ads, and the monitor says UP for a week. Or a migration moved DNS for the production hostname but the monitor was created against a hard-coded IP or a staging alias.
Fix: monitor every hostname a user can land on, through DNS, the same way a user would. Give each of apex, www, and any regional or legacy hostname its own monitor; on the free plan that is still well within the 25-monitor limit. Add a keyword assertion so a parking page's 200 does not pass. For the DNS side, DNS monitoring explains what you can and cannot see from outside, and certificate monitoring catches the related case where the hostname resolves but the certificate belongs to someone else's server.
6. "Welcome to nginx!"
A deploy wipes the virtual host configuration, a container restarts without its mounted config, a new reverse proxy goes live without a route for your hostname. The web server falls back to its default site and serves its stock page: Welcome to nginx!, Apache's It works!, Caddy's empty 200, or your hosting panel's "This site is not configured yet." All of them are 200s. All of them pass a status check, and this one is especially embarrassing because it is often visible on the homepage for hours.
Fix: the same keyword assertion from section 2 catches it, because the default page does not contain your footer text. If you run several sites behind one proxy, a single not_contains assertion for the server's default heading (Welcome to nginx, It works) on each site is a cheap guard. Monitoring behind a reverse proxy covers the proxy-level failures that produce this page.
7. Background work that fails while the website is fine
The site is up. Every HTTP monitor is green. Meanwhile the queue worker crashed on Friday night, so no order confirmation, password reset, or invoice has been sent since; the nightly report never ran; the database backup has not completed in nine days. None of this is visible to an HTTP check because none of it is on the request path. It is the largest class of outage that an all-green uptime dashboard hides, and no amount of keyword tuning fixes it.
Fix: heartbeat monitors (Pro). Each job gets a unique ping URL; the job requests it when it finishes successfully, and CronAlert alerts when the ping does not arrive within the expected interval plus a grace period. The pattern is one line at the end of the job:
run-nightly-report && curl -fsS --max-time 10 --retry 3 https://cronalert.com/api/heartbeat/YOUR-TOKEN The && matters: a failed job must not ping. Cron job heartbeat monitoring is the full guide; background workers and backups are the two most common applications, and grace periods explains how to set the window so a job that runs a few minutes late does not alert.
Prove the monitor can fail
Every fix above shares a test: make the monitor fail on purpose once. Change the keyword to a string that is not on the page and confirm the next check goes red; flip the expected status to 201 and watch it fail; stop the cron job for one interval and wait for the heartbeat alert. A monitor you have never seen fail is a monitor you are trusting on faith. Testing your uptime alerts is a ten-minute drill for exactly this, and it also confirms the alert reaches a channel someone actually reads.
Then write down, next to each monitor's name, what a failure would mean. "Homepage 200" means the CDN is serving. "Homepage contains footer" means the origin rendered a real page. "/api/health contains "db":"ok"" means the database answered. The dashboard is only reassuring when you know what each green dot is actually asserting.
Frequently asked questions
Should I use keyword assertions on every monitor?
On every monitor whose target can return a 200 that is not your page, yes, which is most of them. The exceptions are health endpoints you control that already return a non-200 on failure, and API endpoints where the status code is the contract. Each monitor carries one assertion, so when you want both "contains the footer" and "does not contain an error string" on the same page, that is two monitors.
Does CronAlert follow redirects?
Yes, always, the way a browser does. The status code and body you assert on belong to the final page after redirects. That is why a login redirect produces a false negative and why you cannot set the expected status to 301 or 302.
Can I detect a cached copy being served by a CDN?
Not directly, since HTTP monitors cannot assert on response headers. The practical options are a content-changed monitor on a small endpoint that embeds your build identifier, a keyword on something that is only present in the current release, or a monitor that sends a cache-busting header such as Cache-Control: no-cache if your CDN honours it, so the check always reaches the origin.
Will multi-region checks help with false negatives?
Only for the case where one region's path to your site is broken while another's works, which is a false negative in one place and a false positive in another. Multi-region checks (Team plan) give you a quorum across locations; they do not help with any of the seven cases above, which fool every region the same way.
Which of these are free?
Status checks, custom headers, exact expected status codes, certificate monitoring, and the timeout are on every plan. Keyword, regex, and content-change assertions and heartbeat monitors are Pro at $5/month. Uptime monitoring on a budget has suggestions for getting the most out of the free tier.
Get started
Pick the monitor you trust most, look at what it actually asserts, and ask which of the seven failures above would get past it. Usually at least two would. Create a free CronAlert account, add a keyword to the homepage monitor, point a second monitor at the API the page depends on, and give the one background job you would miss most a heartbeat. Related reading: setting up uptime monitoring, monitor types explained, HTTP status codes for monitoring, and the other side of the coin: false positives.