<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[PingoGo Blog]]></title><description><![CDATA[PingoGo Blog]]></description><link>https://pingogo.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 13 Nov 2026 21:14:27 GMT</lastBuildDate><atom:link href="https://pingogo.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Your uptime monitor says 200 OK. Your site is still broken.]]></title><description><![CDATA[Why status-code checks miss real outages, and how a simple keyword check closes most of the gap]]></description><link>https://pingogo.hashnode.dev/your-uptime-monitor-says-200-ok-your-site-is-still-broken</link><guid isPermaLink="true">https://pingogo.hashnode.dev/your-uptime-monitor-says-200-ok-your-site-is-still-broken</guid><category><![CDATA[monitoring]]></category><category><![CDATA[Devops]]></category><category><![CDATA[webdev]]></category><category><![CDATA[SRE]]></category><category><![CDATA[http]]></category><dc:creator><![CDATA[Max]]></dc:creator><pubDate>Thu, 12 Nov 2026 10:13:40 GMT</pubDate><content:encoded><![CDATA[<p>Most uptime checks ask one question: did the server answer with a 2xx? If yes, the dashboard stays green.</p>
<p>That question is narrower than it looks. A 200 tells you the server says the request succeeded. It doesn't tell you whether the page your visitors got is the page you meant to serve.</p>
<h2>What 200 actually promises</h2>
<p>RFC 9110, the current HTTP spec, defines 200 like this: "The 200 (OK) status code indicates that the request has succeeded." For a GET, the body is "a representation of the target resource" (<a href="https://www.rfc-editor.org/rfc/rfc9110#section-15.3.1">RFC 9110, section 15.3.1</a>).</p>
<p>Notice who decides that. The server. If your app catches an exception and renders a friendly "Something went wrong" template, it can send that with a 200. If a reverse proxy or hosting panel falls back to a default page, that can be a 200 too. HTTP has no idea the content is wrong.</p>
<p>Google runs into the same problem at a much bigger scale. Its crawler docs say that when a 2xx page's content "suggests an error", such as "an empty page or an error message", Search Console reports it as a soft 404 (<a href="https://developers.google.com/search/docs/crawling-indexing/http-network-errors">Google Search Central, HTTP status codes</a>). In other words, even Google doesn't trust 200 by itself. It looks at the content.</p>
<h2>Ways a site breaks while returning 200</h2>
<p>These are common patterns, not a complete list:</p>
<ul>
<li><strong>The app catches its own error.</strong> A framework error boundary or a generic error template renders with status 200 instead of 500.</li>
<li><strong>A dependency is down, the shell isn't.</strong> The layout loads, but the product list, the cart or the login form is empty because the database or an API call failed.</li>
<li><strong>A deploy shipped the wrong thing.</strong> A placeholder page, a "coming soon" template, a default server welcome page, or the staging build.</li>
<li><strong>Something in front of the app answers instead of it.</strong> A hosting parking page, a maintenance page, or a cached copy that is badly out of date.</li>
<li><strong>A redirect lands somewhere you didn't expect.</strong> The monitor follows the redirect, gets a 200 at the end, and reports success, even though the final URL is a login wall or a different domain.</li>
</ul>
<p>In every one of these, a status-only check stays green while real people see a broken page.</p>
<h2>The cheap fix: check for something that should be there</h2>
<p>The simplest upgrade is a keyword check (also called a content check or content match). Along with the status code, the monitor downloads the response body and looks for a string that only appears when the page is actually working. If the string is missing, the check fails, even with a 200.</p>
<p>You don't need a headless browser for this. A plain HTTP GET and a substring search cover a lot of ground.</p>
<h3>Picking a good keyword</h3>
<p>This is where most of the value is, and also where most false alerts come from.</p>
<p><strong>Good choices:</strong></p>
<ul>
<li>Text that is rendered from your data, not hard-coded in the layout. For a shop, a category name that comes from the database is better than the logo text. If the database is down, it disappears.</li>
<li>A stable marker you control, for example a hidden <code>data-health="ok"</code> attribute or a short comment your template writes only when the page rendered fully.</li>
<li>On an API, a field from a real response, like <code>"status":"ok"</code> on a health endpoint that actually checks its dependencies.</li>
</ul>
<p><strong>Bad choices:</strong></p>
<ul>
<li>Your brand name. It's in the header, the footer, the title and probably the error page too.</li>
<li>Marketing copy that changes every campaign. You'll get alerts every time someone edits a headline.</li>
<li>Anything that only appears after JavaScript runs, if the monitor doesn't execute JavaScript (most HTTP monitors don't).</li>
</ul>
<p>A quick test: open your own error page, maintenance page and an empty-database state if you can reproduce it. Your keyword should be missing from all of them and present on the working page.</p>
<h3>An inverse check helps too</h3>
<p>Some monitors also let you alert when a string <em>does</em> appear, such as "Error establishing a database connection" or "maintenance". That's useful when you know the exact failure text. It's a complement to the positive keyword, not a replacement, because you can't list every way a page can fail.</p>
<h2>Limits worth knowing</h2>
<p>Keyword checks are not a full browser test.</p>
<ul>
<li><strong>Single-page apps.</strong> If the server sends an empty <code>&lt;div id="root"&gt;&lt;/div&gt;</code> and everything renders client-side, there's nothing useful in the HTML to match. Monitor an API endpoint the app depends on instead, or use a browser-based synthetic check for the critical flow.</li>
<li><strong>Body size.</strong> Some monitors only read the first part of a large response. Put your marker early in the HTML when you can.</li>
<li><strong>Personalized or A/B-tested pages.</strong> Pick a string that appears in every variant, or check a simpler URL.</li>
<li><strong>It checks one page, not a journey.</strong> Checkout can be broken while the product page looks fine. For flows that make money, a scripted check is worth the extra setup.</li>
</ul>
<h2>A practical setup</h2>
<p>For a typical small site or client site, this covers most real outages without much noise:</p>
<ol>
<li>Monitor the homepage and one page that depends on your database or main API.</li>
<li>Set the expected status range explicitly (for example <code>200-299</code>, or <code>200-399</code> if redirects are fine).</li>
<li>Add a keyword that only shows up when the page rendered from real data.</li>
<li>Require more than one failed check, ideally from more than one location, before alerting. That filters out a single network hiccup.</li>
<li>Send alerts somewhere you'll actually see them quickly.</li>
</ol>
<h2>Disclosure, and where I'm coming from</h2>
<p>I work on <a href="https://www.pingogo.eu/?utm_source=hashnode&amp;utm_medium=blog&amp;utm_campaign=http200_keyword">PingoGo</a>, a small uptime monitor. It does the things above: per-site expected status codes, an optional required keyword, and confirmation from a second region before marking a site down, with alerts going to Telegram. There's a free plan for two sites if you want to try a keyword check on your own site.</p>
<p>If you'd rather use something else, the advice still holds. Whatever tool you use, check whether it supports content matching, and turn it on for at least one page that depends on your backend.</p>
<p>If you want to see what your page returns right now, without signing up, there's a <a href="https://www.pingogo.eu/tools/http-status-checker?utm_source=hashnode&amp;utm_medium=blog&amp;utm_campaign=http200_keyword">free HTTP status checker</a>.</p>
<p><em>I wrote this with help from an AI assistant and reviewed and edited it myself.</em></p>
]]></content:encoded></item></channel></rss>