<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: velprove</title>
    <description>The latest articles on DEV Community by velprove (@velprove).</description>
    <link>https://dev.to/velprove</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3847633%2F11c1a231-027c-4c79-89cc-c40cd77f4834.png</url>
      <title>DEV Community: velprove</title>
      <link>https://dev.to/velprove</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/velprove"/>
    <language>en</language>
    <item>
      <title>Monitor a DigitalOcean App Platform or Droplet Stack</title>
      <dc:creator>velprove</dc:creator>
      <pubDate>Mon, 01 Jun 2026 14:00:04 +0000</pubDate>
      <link>https://dev.to/velprove/monitor-a-digitalocean-app-platform-or-droplet-stack-42h7</link>
      <guid>https://dev.to/velprove/monitor-a-digitalocean-app-platform-or-droplet-stack-42h7</guid>
      <description>&lt;p&gt;&lt;strong&gt;The short version:&lt;/strong&gt; To monitor digitalocean app platform properly you have to know what its three native layers do and do not do, and they are better than most people give them credit for: the App Platform health check, threshold alerts across App Platform, Droplets, and managed databases, and DigitalOcean Uptime, a real first-party external synthetic monitor. The catch is that all three answer the same question, "is the box or process up and reachable," and none of them assert the response body, validate JSON, run a multi-step flow, or sign into your login. So a green App Platform deploy whose internal health check passed and whose URL returns a 200 that DigitalOcean Uptime reads as healthy can still serve a broken build, a blank render, or a page whose managed-database read silently failed. This post maps the exact gap on each of the three DigitalOcean surfaces, App Platform components, raw Droplets, and managed databases, and shows how a free, no-code content-assertion and browser login monitor closes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The conceptual anchor: a green deploy that serves a broken page
&lt;/h2&gt;

&lt;p&gt;Picture the failure this post is about. You push to your connected branch, App Platform rebuilds the component, the build passes, the internal health check confirms the port answers 2xx, and the deploy goes green. DigitalOcean Uptime, pointed at the public URL, reads a 200 and reports the service healthy. Every native signal you have is green. And the page your users load is blank, or it is yesterday's build, or it renders a shell while the read from your managed Postgres returns nothing. Nothing native catches it, because nothing native is looking at the body.&lt;/p&gt;

&lt;p&gt;This is not a hypothetical specific to one bad day. &lt;a href="https://status.digitalocean.com/history" rel="noopener noreferrer"&gt;DigitalOcean's own status history&lt;/a&gt; shows App Platform has had multi-region deploy and build-failure incidents, and the broader class, a deploy that succeeds from the platform's point of view but serves the wrong response, is the steady-state risk between named incidents. The rest of this post is the three DigitalOcean surfaces and the precise native gap on each.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the three native DigitalOcean layers actually do
&lt;/h2&gt;

&lt;p&gt;To monitor digitalocean app platform honestly you have to start by giving the native tooling full credit, because the differentiator is a capability gap, not an absence. There are three layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App Platform health checks.&lt;/strong&gt; These are configurable HTTP or TCP readiness and liveness probes that run internally, from inside DigitalOcean's network, against an &lt;code&gt;http_path&lt;/code&gt; you set. The readiness probe gates the deploy and stops routing traffic to an instance that is not ready. The liveness probe automatically restarts a component whose check fails and emails the account. They are real and you should configure them. What they confirm is narrow: the port answered 2xx from inside the platform. They never leave the network and never read the body for correctness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Threshold metric alerts across all three surfaces.&lt;/strong&gt; App Platform alert policies cover failed and successful deploys (failed-deploy alerting is on by default), CPU, RAM, restart count, request rate, and P95 request duration, delivered to email or Slack. Droplets get the free DigitalOcean Monitoring agent plus Alert Policies: threshold alerts on CPU, memory, disk, and bandwidth. Managed databases get their own alert policies on connection count, CPU, memory, and disk. Every one of these is a threshold on a metric. A metric crossing a line is a useful signal and a different signal from "the response is wrong."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DigitalOcean Uptime.&lt;/strong&gt; This is DigitalOcean's own first-party external synthetic monitor, and it deserves to be named, because the lazy version of this post would pretend it does not exist. Per &lt;a href="https://docs.digitalocean.com/products/uptime/details/features/" rel="noopener noreferrer"&gt;DigitalOcean's Uptime feature docs&lt;/a&gt; , it checks a URL or IP over HTTPS, HTTP, or ICMP from up to 4 global regions, alerts on downtime and latency to email and Slack, monitors SSL certificate expiry on HTTPS checks, and keeps up to 90 days of latency history. It is a competent reachability and latency monitor.&lt;/p&gt;

&lt;p&gt;Side by side, the capability split is the whole story. The App Platform health check and DigitalOcean Uptime each assert reachability and metrics; neither reads the response body, validates JSON, or signs into a login. That body-and-login correctness layer is what an external no-code monitor adds.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;App Platform health check&lt;/th&gt;
&lt;th&gt;DigitalOcean Uptime&lt;/th&gt;
&lt;th&gt;Velprove&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint reachable / 2xx status&lt;/td&gt;
&lt;td&gt;Yes (internal probe)&lt;/td&gt;
&lt;td&gt;Yes (external, up to 4 regions)&lt;/td&gt;
&lt;td&gt;Yes (external, 5 regions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency and SSL expiry alerts&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-restart on failed liveness&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response body / string assertion&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON validation and multi-step flows&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (multi-step up to 3 on free)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No-code browser login monitoring&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The honest gap: reachability green, correctness unverified
&lt;/h2&gt;

&lt;p&gt;Here is the thesis, stated precisely so nobody can read it as an overclaim. All three native layers answer "is the box or process up and reachable?" None of them assert the response body, validate JSON, drive a browser, sign into a login, or run a multi-step flow. DigitalOcean Uptime confirms a 200, acceptable latency, and a valid certificate. Verified against DigitalOcean's own feature docs, it does not do content matching, string matching, JSON validation, or login flows. It treats any status outside the 200-299 range as an outage and stops there.&lt;/p&gt;

&lt;p&gt;The difference between DigitalOcean's health check and external monitoring is the difference between reachability and correctness: the App Platform health check confirms the component's port answers 2xx from inside the platform, while an external content monitor confirms the public response actually renders the right body. Reachability failures change the status code, so the native tools catch them. Correctness failures leave the status code at 200 and change only the body, so the native tools miss them entirely. A successful App Platform deploy can serve a 200 with a blank render, the wrong build, or a page whose database read silently returned empty, and your health check, your metric alerts, and DigitalOcean Uptime all stay green. The layer that reads the body is the external no-code assertion monitor, and it is the only thing in this stack that catches a logically broken 200.&lt;/p&gt;

&lt;p&gt;One DigitalOcean-specific surface worth a single sentence: if you run worker components or DO Jobs, their PRE-, POST-, and FAILED-DEPLOY hooks are platform-side lifecycle events, not response correctness, and the way to make a worker observable from outside is the same freshness-endpoint approach the rest of this stack uses. The freshness-endpoint and build-SHA assertion mechanic itself is covered in &lt;a href="https://velprove.com/blog/api-health-check-patterns" rel="noopener noreferrer"&gt;our API health-check patterns guide&lt;/a&gt; , so this post references it rather than re-teaching it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitor DigitalOcean App Platform components: assert the body the deploy left behind
&lt;/h2&gt;

&lt;p&gt;To monitor a DigitalOcean App Platform app for correctness, point an external HTTP monitor at the component's public URL and assert on the response body, not just the status code. On an App Platform component the native gap is the deploy that succeeds but serves the wrong response, and the fix is a content assertion on the public URL. In the Velprove wizard, set the check type to an HTTP monitor, point it at your component's URL, and on the Verify step add two Success Conditions: a status code of 200, and a Response Body Contains assertion on a string your correct page always renders. Pick a string that is load-bearing, a known piece of copy, a marker your template emits, or, better, a value that only appears when a real read succeeded, so a blank or shell render fails the check even though the status code is 200.&lt;/p&gt;

&lt;p&gt;For the deploy-skew case specifically, where the build succeeded but promoted the wrong code, the pattern is a freshness or build-SHA assertion against a light &lt;code&gt;/version&lt;/code&gt; route your app exposes. That route and the multi-step build-SHA comparison mechanic are owned by &lt;a href="https://velprove.com/blog/api-health-check-patterns" rel="noopener noreferrer"&gt;the API health-check patterns guide&lt;/a&gt; , so configure it there and point a Velprove monitor at the result. The point on App Platform is only this: the platform tells you the deploy finished, your assertion tells you the deploy finished correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Droplets and managed databases: same blind spot, different native tool
&lt;/h2&gt;

&lt;p&gt;You can monitor a DigitalOcean Droplet, an App Platform component, and a Managed Database with the same external content monitor: each has a different native tool (Droplet metric Alert Policies, App Platform alert policies, Managed Database alert policies) but the same blind spot, because none of them assert the response body. On a Droplet, a raw VM, the native monitoring is the agent-based metric story: the free Monitoring agent plus Alert Policies on CPU, memory, disk, and bandwidth. Those are excellent for a runaway process or a full disk. They say nothing about whether the nginx in front of your app is returning your application or a default welcome page, or a 200 error page from a misconfigured reverse proxy. You point the same Velprove content-assertion monitor at the Droplet's public URL, and the metric-green-but-content-wrong case becomes visible.&lt;/p&gt;

&lt;p&gt;On a managed database the native alert policies watch connection count, CPU, memory, and disk on the database itself. They cannot tell you the read your application performs through that database came back with the right data, or came back at all. A connection-pool exhaustion, a bad migration, or a permissions change can leave every database metric green while your app's query silently returns empty. The external probe that catches that does not touch the database directly. It asserts a string on a page or endpoint whose render depends on a real read: the metric stays green, the assertion goes red, and you learn the read broke before your customers file the ticket.&lt;/p&gt;

&lt;p&gt;Note one thing this post is not claiming. Unlike a platform that sleeps idle services, DigitalOcean App Platform components stay warm, so the gap here is not cold-start or idle-sleep latency, the way it is on some sibling platforms (the idle-sleep contrast is covered in &lt;a href="https://velprove.com/blog/monitor-railway-app" rel="noopener noreferrer"&gt;the Railway platform-layer guide&lt;/a&gt; ). The DigitalOcean gap is purely the ceiling of native synthetic monitoring: reachability and metrics are covered, response correctness is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser login monitor on the real signed-in path
&lt;/h2&gt;

&lt;p&gt;Content assertions prove the public surface renders correctly. They do not prove a real user can sign in and see their data, and on a DigitalOcean-hosted SaaS that is the failure that costs you customers. This is where the no-code login monitoring sits. Velprove's browser login monitor opens a real browser, signs in as a dedicated low-privilege test user, follows the post-login redirect, and asserts a string on the landing page that only renders if a real read from your managed database actually succeeded.&lt;/p&gt;

&lt;p&gt;The setup is no-code: in the wizard you give the login URL, the test user's credentials, and, under Customize detection, switch Success verification from the default URL-change to "Page contains text" set to a post-login data string, a customer name, an invoice ID, a known plan label. A component can return 200 with an intact page shell while the database read behind the dashboard fails. A text-present assertion on post-login content catches that; a status-code probe never will. To be precise about the claim: Velprove is not the only tool that offers free browser checks, but the combination here, free and no-code login monitoring that signs into your own login with no Playwright code to write, is the differentiator.&lt;/p&gt;

&lt;p&gt;Use a dedicated test account with the smallest permissions that still renders a real data-backed page, never production admin credentials. The browser login monitor is free on every plan, including the free plan, at a 15-minute interval, which is enough to catch a multi-hour database-backed outage and a login regression within one window.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this compares to the sibling platform guides
&lt;/h2&gt;

&lt;p&gt;The platform-sibling guides share this shape, native reachability is solid, native body-correctness is the gap, and they differ on the exact native wedge. Heroku charges for native alerting, so external monitoring there is partly a cost play; DigitalOcean is different, because DigitalOcean actually sells you a competent external monitor in DigitalOcean Uptime, separately, and it still cannot assert your body, so the wedge here is capability, not price (the cost framing is in &lt;a href="https://velprove.com/blog/monitor-heroku-app" rel="noopener noreferrer"&gt;the Heroku platform-layer guide&lt;/a&gt; ). For the broader pattern shared across managed-host platforms, see &lt;a href="https://velprove.com/blog/monitor-render-hosted-app" rel="noopener noreferrer"&gt;the Render platform-layer guide&lt;/a&gt; ; the DigitalOcean version is distinguished by the three-surface split (App Platform, Droplet, managed database) and by the fact that DigitalOcean's first-party synthetic monitor sets the native ceiling higher than most while still stopping at reachability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;The Velprove free plan covers 10 monitors total at a 5-minute HTTP interval, one browser login monitor at a 15-minute interval, multi-step API monitors up to 3 steps, 5 global regions to choose from (one per monitor), email alerts, SSL expiry monitoring, and 1 status page. Commercial use is allowed on every plan, including free. No credit card required.&lt;/p&gt;

&lt;p&gt;That is enough to land the DigitalOcean correctness layer for a single production app: a content-assertion HTTP monitor on your App Platform component or Droplet URL, a freshness or build-SHA assertion on a &lt;code&gt;/version&lt;/code&gt; route, and one browser login monitor on the signed-in path. Keep your App Platform health checks, your metric alert policies, and DigitalOcean Uptime turned on; this sits on top of them and reads the body they never read. &lt;a href="https://velprove.com/signup" rel="noopener noreferrer"&gt;Start with the free plan&lt;/a&gt;. The first monitor takes about three minutes to configure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Isn't DigitalOcean Uptime or the App Platform health check enough?
&lt;/h3&gt;

&lt;p&gt;They are real and worth turning on, but they answer a narrower question than most people assume. The App Platform health check is an internal HTTP or TCP readiness and liveness probe: it gates the deploy and auto-restarts a component whose port stops answering 2xx, and it emails the account. DigitalOcean Uptime is a genuine external synthetic monitor that checks a URL or IP over HTTPS, HTTP, or ICMP from up to 4 regions and alerts on downtime, latency, and SSL certificate expiry. What none of them do is assert your response body, validate JSON, run a multi-step flow, or sign into your login. They confirm the box is up and reachable and the certificate is valid. They do not confirm the page renders correct content. That correctness layer is what an external no-code assertion monitor adds on top.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I monitor DigitalOcean App Platform on the Velprove free plan?
&lt;/h3&gt;

&lt;p&gt;Yes. The Velprove free plan covers 10 monitors total at a 5-minute HTTP interval, one browser login monitor at a 15-minute interval, multi-step API monitors up to 3 steps, email alerts, SSL expiry monitoring, and 1 status page. Commercial use is allowed and no credit card is required. That is enough to put a content-assertion HTTP monitor on your App Platform component URL, a freshness or build-SHA assertion on a &lt;code&gt;/version&lt;/code&gt; route, and one browser login monitor on the signed-in path of a DigitalOcean-hosted SaaS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is monitoring a DigitalOcean Droplet different from monitoring an App Platform component?
&lt;/h3&gt;

&lt;p&gt;The native tools differ, the blind spot is the same. A Droplet is a raw VM, so its native monitoring is the free DigitalOcean Monitoring agent plus Alert Policies: threshold alerts on CPU, memory, disk, and bandwidth to email or Slack. An App Platform component is managed, so its native monitoring is App Platform alert policies (deploy outcome, CPU, RAM, restart count, request rate, P95 latency) plus the internal health check. Both are metrics and reachability signals. Neither asserts that the response your Droplet's nginx or your App Platform service returns is the correct content. You point a Velprove content-assertion or browser login monitor at the public URL in front of either surface, and the setup is the same regardless of which DigitalOcean product is behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about a DigitalOcean managed database? Native alerts already cover it.
&lt;/h3&gt;

&lt;p&gt;Managed-database alert policies watch connection count, CPU, memory, and disk on the database itself, which is genuinely useful. They do not tell you that the read your app performs through that database returned the right data, or returned anything at all. A managed database can be green on every metric while a connection-pool exhaustion, a bad migration, or a permissions change makes your app's query silently return empty. The external probe that catches that is a content assertion on a page or endpoint whose render depends on a real database read: assert that a known string only that read produces is present. The database metric stays green; your assertion goes red.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does DigitalOcean Uptime check the response body or sign into my login?
&lt;/h3&gt;

&lt;p&gt;No. DigitalOcean Uptime confirms the endpoint is reachable, measures latency from up to 4 regions, and on HTTPS checks watches the SSL certificate expiry, with 90 days of latency history. It treats any HTTP status outside the 200-299 range as an outage. It does not do content or string matching, JSON validation, multi-step request chains, or browser-driven login flows. So a deploy that succeeds, passes the internal health check, and returns a 200 that DigitalOcean Uptime reads as healthy can still be serving a blank render, a stale build, or a page whose database read silently failed. The body-level and login-level correctness check is the no-code assertion and browser login monitor's job, not DigitalOcean Uptime's.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>uptime</category>
    </item>
    <item>
      <title>Your GraphQL API Returns 200 While It's Down. Here's How to Catch It.</title>
      <dc:creator>velprove</dc:creator>
      <pubDate>Sun, 31 May 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/velprove/your-graphql-api-returns-200-while-its-down-heres-how-to-catch-it-2idb</link>
      <guid>https://dev.to/velprove/your-graphql-api-returns-200-while-its-down-heres-how-to-catch-it-2idb</guid>
      <description>&lt;p&gt;** The 30-second version: A GraphQL endpoint can return HTTP &lt;code&gt;200&lt;/code&gt; while it is functionally down. When a field or a resolver fails on a well-formed request, GraphQL reports it inside the response body as a top-level &lt;code&gt;errors&lt;/code&gt; array, often with &lt;code&gt;null&lt;/code&gt; sitting in &lt;code&gt;data&lt;/code&gt; where the real value should be. The HTTP status stays &lt;code&gt;200&lt;/code&gt;, so a plain status check stays green. The fix is a body-content assertion: confirm &lt;code&gt;200&lt;/code&gt; AND that &lt;code&gt;$.errors[*]&lt;/code&gt; has no matches AND that &lt;code&gt;$.data.&amp;lt;criticalField&amp;gt;&lt;/code&gt; is not null. That is exactly what Velprove's free, no-code multi-step API monitor does: it asserts on the response body, not just the status code. **&lt;/p&gt;

&lt;p&gt;If you run a GraphQL API, your uptime monitor is probably lying to you. Not because the tool is bad, but because GraphQL breaks the assumption every status-only check is built on: that a &lt;code&gt;200&lt;/code&gt; means the request worked. For REST that assumption mostly holds. For GraphQL it does not. A query can fail in a way that takes your whole feature down and still hand back a tidy &lt;code&gt;200 OK&lt;/code&gt;. To &lt;strong&gt;monitor GraphQL API uptime&lt;/strong&gt; for real, you have to read the body.&lt;/p&gt;

&lt;p&gt;This is the same blind spot we wrote about more generally in &lt;a href="https://velprove.com/blog/why-uptime-monitors-miss-outages" rel="noopener noreferrer"&gt;why a 200 OK can hide an outage&lt;/a&gt; . GraphQL is the sharpest example of it, because the 200-with-an-error is not an edge case here. It is the documented, default, by-design behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why GraphQL returns 200 when a field fails
&lt;/h2&gt;

&lt;p&gt;Start with the scope, because the common overstatement ( "GraphQL always returns 200") is wrong and will lead you to build the wrong monitor. The 200-with-errors behavior applies to one specific situation: a well-formed request that the server accepts and executes, responding with the &lt;code&gt;application/json&lt;/code&gt; media type, where a field or a resolver fails during execution.&lt;/p&gt;

&lt;p&gt;In that case the transport did its job. Your query parsed, validated, and ran. One of the resolvers threw, or returned &lt;code&gt;null&lt;/code&gt; for a non-nullable field, or an upstream the resolver called timed out. The server has a valid HTTP response to send you, so it sends &lt;code&gt;200&lt;/code&gt; and reports the failure in the body. The response carries a top-level &lt;code&gt;errors&lt;/code&gt; array describing what broke, and a &lt;code&gt;data&lt;/code&gt; object that holds &lt;code&gt;null&lt;/code&gt; where the failed field should have been.&lt;/p&gt;

&lt;p&gt;This is described in the &lt;a href="https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md" rel="noopener noreferrer"&gt;graphql-over-http specification&lt;/a&gt; , and the clearest practitioner write-up is Nigel Sampson's &lt;a href="https://compiledexperience.com/blog/posts/200-Not-OK" rel="noopener noreferrer"&gt;"GraphQL and 200 Not OK"&lt;/a&gt; (2020), which frames the problem exactly the way a monitoring engineer runs into it. Sasha Solomon's "200 OK! Error Handling in GraphQL" (2019) covers the same ground from the schema-design side. The short version: in GraphQL, the HTTP status describes the transport, and the &lt;code&gt;errors&lt;/code&gt; array describes your query. A monitor that only reads the status is reading the wrong layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three failure modes a status check misses
&lt;/h2&gt;

&lt;p&gt;There are three shapes this takes in production, and a status-only check is blind to all three. Each one returns &lt;code&gt;200&lt;/code&gt;. The samples below are responses your own GraphQL API would send back. Names and fields are illustrative.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Field error with a populated errors array
&lt;/h3&gt;

&lt;p&gt;A resolver throws. The field it was responsible for comes back &lt;code&gt;null&lt;/code&gt;, and the failure shows up in &lt;code&gt;errors&lt;/code&gt;. The status is still &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Your dashboard's "who am I" query just failed for every signed-in user. The status check sees &lt;code&gt;200&lt;/code&gt; and a non-empty body and reports green.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A critical field returns null while siblings resolve
&lt;/h3&gt;

&lt;p&gt;The query mostly works. One important field goes &lt;code&gt;null&lt;/code&gt; because its resolver failed, while the cheap fields around it resolve fine. Sometimes there is an &lt;code&gt;errors&lt;/code&gt; entry, sometimes the resolver swallowed the error and just returned &lt;code&gt;null&lt;/code&gt;. Either way the body looks populated.&lt;/p&gt;

&lt;p&gt;The product page renders with a name and no price. Nothing is "down" by any status-code measure, but you cannot sell the thing. An assertion on &lt;code&gt;$.data.product.price&lt;/code&gt; being non-null is the only check that catches this.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Partial data, the page half-loads
&lt;/h3&gt;

&lt;p&gt;This is partial success: &lt;code&gt;data&lt;/code&gt; and &lt;code&gt;errors&lt;/code&gt; in the same response. The fields that worked are in &lt;code&gt;data&lt;/code&gt;, the ones that failed are &lt;code&gt;null&lt;/code&gt;, and &lt;code&gt;errors&lt;/code&gt; explains the gaps. The graphql-over-http spec treats this as normal, expected behavior, not an error condition for the transport.&lt;/p&gt;

&lt;p&gt;Half the page loads. The order details are there, the recommendations rail is empty. The response is &lt;code&gt;200&lt;/code&gt; with a healthy-looking &lt;code&gt;data&lt;/code&gt; object, and the only signal that something broke is the &lt;code&gt;errors&lt;/code&gt; array nobody is reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it's actually a 4xx or 5xx (and when it isn't)
&lt;/h2&gt;

&lt;p&gt;GraphQL does use real HTTP error codes, just not for the failures above. Knowing where the line falls keeps you from building a monitor on a false assumption. There are three broad cases where the status does carry the signal, and one important divergence between the spec and what servers actually do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parse and validation errors.&lt;/strong&gt; If your query is malformed, or asks for a field that does not exist, that is a request error caught before execution. Here the spec and real servers part ways. The &lt;a href="https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md" rel="noopener noreferrer"&gt;graphql-over-http spec&lt;/a&gt; recommends returning &lt;code&gt;200&lt;/code&gt; even for request errors when the response uses the &lt;code&gt;application/json&lt;/code&gt; media type. In practice &lt;a href="https://www.apollographql.com/docs/apollo-server/data/errors" rel="noopener noreferrer"&gt;Apollo Server returns 400&lt;/a&gt; for parse and validation errors. So do not assume &lt;code&gt;400&lt;/code&gt; is universal, and do not assume &lt;code&gt;200&lt;/code&gt; is either. It depends on the server. For a monitor this is fine, because a malformed canary query is your bug to fix before you ship the monitor, not a production signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invalid variables.&lt;/strong&gt; One trap worth a single sentence: older Apollo Server 4 returned &lt;code&gt;200&lt;/code&gt; when a variable failed coercion, which meant a bad-input failure hid behind a success status. Current Apollo fixes this with &lt;code&gt;status400ForVariableCoercionErrors&lt;/code&gt;, which returns &lt;code&gt;400&lt;/code&gt; and is the default in Apollo Server 5.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transport and server crashes.&lt;/strong&gt; If the process is down, the load balancer has no healthy backend, or an upstream gateway times out, you get a real &lt;code&gt;5xx&lt;/code&gt; (or a connection failure). This is the one case a status-only check reliably catches, and it is the minority of GraphQL outages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The newer media type.&lt;/strong&gt; The spec defines a second media type, &lt;code&gt;application/graphql-response+json&lt;/code&gt;, which &lt;em&gt;may&lt;/em&gt; use non-200 statuses for errors, and the draft even sketches a non-standard &lt;code&gt;294&lt;/code&gt; "Partial Success" code. Treat that as emerging, not deployed. The spec is still at Draft stage, and most servers in the wild still answer with &lt;code&gt;application/json&lt;/code&gt; and &lt;code&gt;200&lt;/code&gt;. Build for what your server actually sends today.&lt;/p&gt;

&lt;p&gt;Net of all of this: the real GraphQL blind spot is the field error that resolves to &lt;code&gt;200&lt;/code&gt; with a populated &lt;code&gt;errors&lt;/code&gt; array. No status code will surface it. You have to read the body.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to monitor a GraphQL API for the 200-that-lies (Velprove, no code)
&lt;/h2&gt;

&lt;p&gt;Velprove's free, no-code multi-step API monitor asserts on the response body, not just the status code. That is the whole game for GraphQL. The browser login monitor is the differentiator we lead with for sign-in flows, but the right tool here is the API monitor with a JSON-path assertion on the &lt;code&gt;errors&lt;/code&gt; array. Here is the shape, in four steps, no config files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1. POST your GraphQL endpoint with a small canary query.&lt;/strong&gt; Create an API monitor that sends a &lt;code&gt;POST&lt;/code&gt; to your single GraphQL URL (something like &lt;code&gt;/graphql&lt;/code&gt;) with a small, read-only query in the request body. Keep it cheap and stable. Ask for the one or two fields you most need to be alive. Run it with a dedicated low-privilege monitoring account, never real admin credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2. Assert the status code is 200.&lt;/strong&gt; This is the baseline that catches the transport and crash failures from the section above. It is necessary and, on its own, nowhere near sufficient.&lt;/p&gt;

&lt;p&gt;** Step 3. Assert the JSON path &lt;code&gt;$.errors[*]&lt;/code&gt; has no matches. ** This is the assertion that turns a status check into a real GraphQL health check. The &lt;code&gt;[*]&lt;/code&gt; matches the entries inside the array, so it passes when &lt;code&gt;errors&lt;/code&gt; is absent or an empty &lt;code&gt;[]&lt;/code&gt;, and fails the moment any error entry appears, even though the status is still &lt;code&gt;200&lt;/code&gt;. It catches failure modes 1 and 3 above.&lt;/p&gt;

&lt;p&gt;** Step 4. Assert &lt;code&gt;$.data.&amp;lt;criticalField&amp;gt;&lt;/code&gt; is not null. ** Point this at the field your product genuinely depends on, for example &lt;code&gt;$.data.currentUser.id&lt;/code&gt; or &lt;code&gt;$.data.product.price&lt;/code&gt;. Use a not-null assertion, not a bare existence check. A field can be present and still &lt;code&gt;null&lt;/code&gt;, which is exactly failure mode 2, where a critical field quietly goes &lt;code&gt;null&lt;/code&gt; and the resolver swallowed the error so &lt;code&gt;errors&lt;/code&gt; stays empty. Belt and suspenders: assert no error entries and a non-null value for the field you care about.&lt;/p&gt;

&lt;p&gt;That four-assertion pattern is the entire GraphQL-specific part. The mechanism underneath it, how a monitor sends a request body, reads the JSON response, and runs JSON-path assertions, is the same engine you would use for any API. If you want to extend this into a token-then-query flow, or chain several queries, that is just &lt;a href="https://velprove.com/blog/multi-step-api-monitoring-guide" rel="noopener noreferrer"&gt;chaining and JSON-path assertions in a multi-step API monitor&lt;/a&gt; , and that guide teaches the mechanism end to end. This post only adds the GraphQL assertion shape on top of it. All of this runs on the free plan, from 5 regions, with commercial use allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A GraphQL data probe is a different layer than a /healthz endpoint
&lt;/h2&gt;

&lt;p&gt;It is tempting to think you already cover this because you have a &lt;code&gt;/healthz&lt;/code&gt; endpoint. You do not. They are different layers and you want both.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;/healthz&lt;/code&gt; probe is an endpoint you deliberately build to report health. It returns &lt;code&gt;200&lt;/code&gt; and a small body that says "I am up," usually after checking a database connection and a couple of dependencies. It is a self-report. The patterns for designing one are covered in our note on why &lt;a href="https://velprove.com/blog/api-health-check-patterns" rel="noopener noreferrer"&gt;a /healthz probe is a different layer&lt;/a&gt; .&lt;/p&gt;

&lt;p&gt;A GraphQL 200-with-errors is the opposite situation. It is not a special health endpoint. It is your normal data endpoint, the one your app actually queries, telling you it is fine while a field underneath it is broken. A green &lt;code&gt;/healthz&lt;/code&gt; can sit right next to a GraphQL query that returns &lt;code&gt;null&lt;/code&gt; for the field that pays your bills. The health endpoint reports the service's opinion of itself. The canary query reports what a real client actually gets back. Monitor both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does my GraphQL API return 200 when there's an error?
&lt;/h3&gt;

&lt;p&gt;When the request itself is well-formed and the server responds with the &lt;code&gt;application/json&lt;/code&gt; media type, GraphQL signals field-level and resolver-level failures inside the response body, not in the HTTP status. The transport succeeded, so the status stays &lt;code&gt;200&lt;/code&gt;. The failure is reported as an entry in a top-level &lt;code&gt;errors&lt;/code&gt; array, usually alongside a &lt;code&gt;data&lt;/code&gt; object that holds &lt;code&gt;null&lt;/code&gt; where the failed field should have been. The graphql-over-http spec describes this behavior, and most servers, including Apollo Server in its default configuration, follow it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's in the GraphQL errors array?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;errors&lt;/code&gt; array is a top-level field in a GraphQL response. Each entry is an object with a human-readable &lt;code&gt;message&lt;/code&gt;, and usually a &lt;code&gt;locations&lt;/code&gt; array pointing at the spot in the query that failed, a &lt;code&gt;path&lt;/code&gt; array naming the response field that errored, and an &lt;code&gt;extensions&lt;/code&gt; object that servers like Apollo use to carry a machine-readable code such as &lt;code&gt;INTERNAL_SERVER_ERROR&lt;/code&gt; or &lt;code&gt;UNAUTHENTICATED&lt;/code&gt;. When &lt;code&gt;errors&lt;/code&gt; is present and non-empty, at least one part of your query did not resolve correctly, even though the HTTP status is &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a GraphQL response have both data and errors?
&lt;/h3&gt;

&lt;p&gt;Yes. This is called partial success, and it is normal GraphQL behavior. If one field's resolver throws while its siblings resolve fine, the server returns a &lt;code&gt;data&lt;/code&gt; object containing the fields that worked plus &lt;code&gt;null&lt;/code&gt; for the field that failed, and an &lt;code&gt;errors&lt;/code&gt; array describing what went wrong. A status check sees &lt;code&gt;200&lt;/code&gt; and a non-empty body and reports the API as healthy. The page is half-broken. This is the single most important reason to assert on the body, not the status.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does GraphQL ever return a 4xx or 5xx?
&lt;/h3&gt;

&lt;p&gt;Yes, for failures that happen before execution or in the transport. A malformed query or a request body that fails to parse is a request error, and while the graphql-over-http spec recommends &lt;code&gt;200&lt;/code&gt; under &lt;code&gt;application/json&lt;/code&gt;, Apollo Server actually returns &lt;code&gt;400&lt;/code&gt; for parse and validation errors. Invalid variable values return &lt;code&gt;400&lt;/code&gt; in current Apollo when &lt;code&gt;status400ForVariableCoercionErrors&lt;/code&gt; is on, which is the default in Apollo Server 5. A crashed server or an upstream that times out returns a &lt;code&gt;5xx&lt;/code&gt;. The gap a status check cannot see is the field error that resolves to &lt;code&gt;200&lt;/code&gt; with a populated &lt;code&gt;errors&lt;/code&gt; array.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I alert on a GraphQL errors array if the status is 200?
&lt;/h3&gt;

&lt;p&gt;Use a monitor that asserts on the response body, not just the HTTP status. POST a small canary query to your GraphQL endpoint, then add three assertions: status code equals &lt;code&gt;200&lt;/code&gt;, the JSON path &lt;code&gt;$.errors[*]&lt;/code&gt; has no matches, and the JSON path &lt;code&gt;$.data.&amp;lt;criticalField&amp;gt;&lt;/code&gt; is not null. If the &lt;code&gt;errors&lt;/code&gt; array fills in or your critical field goes &lt;code&gt;null&lt;/code&gt;, the monitor fails even though the status is still &lt;code&gt;200&lt;/code&gt;. Velprove's free, no-code multi-step API monitor asserts on the response body, not just the status code.&lt;/p&gt;

&lt;h3&gt;
  
  
  What query should I use to monitor a GraphQL endpoint?
&lt;/h3&gt;

&lt;p&gt;Use a small read-only canary query that touches the field you care about most, run with a dedicated low-privilege monitoring account rather than real admin credentials. A good canary asks for one critical field and maybe one stable identifier, for example a viewer or health-style query that returns a known id. Keep it cheap so it does not load your resolvers, keep it stable so it does not break on unrelated schema changes, and assert that the one critical field comes back &lt;code&gt;null&lt;/code&gt;-free with an empty &lt;code&gt;errors&lt;/code&gt; array.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://velprove.com/signup" rel="noopener noreferrer"&gt;Set up a free GraphQL monitor with Velprove&lt;/a&gt; . POST a canary query, assert &lt;code&gt;200&lt;/code&gt; AND no &lt;code&gt;errors&lt;/code&gt; entries AND a non-null critical field, all no-code, from 5 regions, commercial use allowed. The next time a resolver fails behind a &lt;code&gt;200 OK&lt;/code&gt;, you hear about it before your users do.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>uptime</category>
    </item>
    <item>
      <title>Why Jetpack and ManageWP Report False Downtime: The Two Failure Modes and the Fix</title>
      <dc:creator>velprove</dc:creator>
      <pubDate>Fri, 29 May 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/velprove/why-jetpack-and-managewp-report-false-downtime-the-two-failure-modes-and-the-fix-9a1</link>
      <guid>https://dev.to/velprove/why-jetpack-and-managewp-report-false-downtime-the-two-failure-modes-and-the-fix-9a1</guid>
      <description>&lt;p&gt;&lt;strong&gt;The pattern.&lt;/strong&gt; Jetpack and ManageWP false-report downtime for two reasons: a roughly ten-second timeout that trips on a slow shared-hosting load (or a firewall and security plugin that blocks the probe), and a homepage HTTP 200 check that stays green while a broken checkout or locked-out wp-admin sits behind it. The durable fix is depth, not a different external vendor: allowlist the probe to stop the false alarms, then monitor what a real user does. Velprove's browser login monitor signs into your own wp-login.php and asserts a logged-in-only string, so it fails the moment wp-admin breaks instead of reporting a green homepage.&lt;/p&gt;

&lt;p&gt;To put numbers on it: Jetpack checks from WordPress.com servers in the United States every five minutes via an HTTP HEAD request, and ManageWP checks from its own external network on a similar interval. Both are external checks, and both fail in the same two opposite directions. They cry wolf, sending a phantom "your site is down" email while real visitors load the page fine. And they go blind, staying green while a real outage runs on a part of the site the homepage check never looks at. You can &lt;a href="https://velprove.com/signup" rel="noopener noreferrer"&gt;build all of this on the free plan&lt;/a&gt;, no credit card required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two ways a built-in WordPress uptime monitor sends false reports
&lt;/h2&gt;

&lt;p&gt;Jetpack's Downtime Monitor and ManageWP's uptime monitor are the two most common built-in options a WordPress owner reaches for. Both are external checks. Jetpack, per &lt;a href="https://jetpack.com/support/monitor/" rel="noopener noreferrer"&gt;its own support documentation&lt;/a&gt; , runs from WordPress.com servers: "one of our servers will start checking your site every five minutes," and it "pings your site's homepage every five minutes, via a HTTP HEAD request." ManageWP checks externally on a similar interval. Neither one is a process living on your server. They both reach in from the public internet, which is the right place to monitor from.&lt;/p&gt;

&lt;p&gt;That shared design is exactly why both fail the same two ways. The first failure is a false alarm, a cry wolf. The monitor sends you a "site is down" email while your site is up and serving real visitors. The second failure is the opposite, a silent miss. The monitor stays green and says nothing while a real outage is happening on a part of the site the monitor never looks at.&lt;/p&gt;

&lt;p&gt;These two failures pull in opposite directions, and that is what makes a built-in homepage monitor so frustrating to trust. It pages you when nothing is wrong, and it stays quiet when something is. Over a few weeks of phantom emails, the natural human response is to stop reading the alerts, which is the worst possible outcome: now you have a monitor that both lies to you and gets ignored. The rest of this post takes the two failure modes one at a time, with the real complaints and vendor quotes that document them, and then lands on the fix that addresses both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cry wolf, part one: the ten-second timeout on slow hosting
&lt;/h2&gt;

&lt;p&gt;The most common false-down alert is a timeout. Jetpack decides your site is down if it does not answer within about ten seconds on a check. On shared hosting, a single request can blow past that window when a neighbor on the box is spiking, when an uncached page has to be rebuilt, or when a backup job is running, even though real visitors with warm caches never notice.&lt;/p&gt;

&lt;p&gt;This is not a guess. A WordPress.org support thread from September 2024 captures the experience precisely. User @philipgilson &lt;a href="https://wordpress.org/support/topic/jetpack-reports-site-down-repeatedly/" rel="noopener noreferrer"&gt;posted on the WordPress.org forums&lt;/a&gt; (last modified 2024-09-22):&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"I am repeatedly getting emails from JetPack that my website appears to be down... Then another saying it's back online. This happens a lot. however when I try to load the website it's not slow to load and loads fine. Error reference: 234058489/intermittent." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;A site that is genuinely down does not come back online thirty seconds later, over and over, while loading fine the whole time in a browser. That up-down-up flapping with an &lt;code&gt;/intermittent&lt;/code&gt; error reference is the signature of a timeout, not an outage. And Jetpack's own staff confirm the mechanism. In the same complaint family, an Automattic staffer explained it directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"If your site is slow to load, it could trigger a notice that it is down... the site may actually be loading, but it's just slow, and Jetpack thinks that this slowtime is instead a sign that the site is offline." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same thread documents the exact threshold and the fact that you cannot change it: "the Jetpack requests timed out, meaning that your site does not respond to these requests after 10 seconds," and "there's no way to adjust the time that the Monitor checks your site, it's either on or off." So the timeout is fixed, the threshold is short, and on slow hosting it trips on a load that a human would happily wait out. This is a real, persistent, vendor-acknowledged behavior, not a recent regression, and it is the number-one source of phantom "down" emails.&lt;/p&gt;

&lt;p&gt;The important diagnostic move here is to separate a false alarm from a site that genuinely keeps falling over. If your site really is going down on a recurring basis, the timeout alert is correct and the problem is upstream of the monitor. We walk through the real causes of a recurring outage, from memory limits to plugin conflicts to host throttling, in &lt;a href="https://velprove.com/blog/wordpress-site-keeps-going-down" rel="noopener noreferrer"&gt;why your WordPress site keeps going down&lt;/a&gt; . If, instead, the site loads fine every time you check and the alerts flap up and down, you are looking at a cry-wolf timeout, and the fix is not to keep restarting things. It is to monitor differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cry wolf, part two: your firewall or security plugin blocks the probe IPs
&lt;/h2&gt;

&lt;p&gt;The second false-down cause is a block. The probe never reaches WordPress because something on your side stops it at the door: a host firewall rule, a security plugin like Wordfence, a geoIP block, or a rate limiter that decides a request hitting your homepage every five minutes from the same place looks like a bot. The page loads fine for you because your request is not the one being blocked. The probe is.&lt;/p&gt;

&lt;p&gt;For Jetpack specifically, there is an extra dependency that makes this worse: the connection layer runs through &lt;code&gt;xmlrpc.php&lt;/code&gt;. A WordPress.org thread from early 2022 shows the volume this can reach. User @johnnyivan &lt;a href="https://wordpress.org/support/topic/jetpack-sending-hundreds-of-emails-saying-my-sites-down-then-back-up/" rel="noopener noreferrer"&gt;reported on the WordPress.org forums&lt;/a&gt; (thread last modified 2022-03):&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"For a couple of weeks, JetPack's sending me hundreds of emails saying my site's down, then back up again. Error reference: 142945637/intermittent... I contacted my hosting provider and they can see nothing wrong, and neither can I." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;An Automattic staffer in that thread named the cause:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"When we try to test your site's xmlrpc.php file (which Jetpack uses to communicate with your site), it is timing out... some hosts block connection requests to that file." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is the tension. Many WordPress owners deliberately block or disable &lt;code&gt;xmlrpc.php&lt;/code&gt;, because it is a well-known target for brute-force login attempts and pingback amplification attacks. That is a defensible, even recommended, security posture. But the moment you do it, Jetpack Monitor loses its connection path and starts emailing you phantom downtime. You did the right thing for security and got punished with a noisy monitor. Blocking harder does not fix it, and unblocking &lt;code&gt;xmlrpc.php&lt;/code&gt; weakens your security to satisfy a monitor, which is backwards.&lt;/p&gt;

&lt;p&gt;ManageWP hits the same wall from a different angle. It checks from an external network, and that external probe gets firewalled like any other outside request. A common symptom is a timeout error of the form "Response timeout, did not receive response for 30sec" thrown against a site that is loading perfectly in a browser. A security forum discussion of this exact error describes the cause plainly: a response timeout like that "usually indicates that some security configuration is blocking requests." (We are not naming ManageWP's backend probe vendor here, because the only sourcing for that is a single third-party forum, not a ManageWP document. The point stands without it: ManageWP checks externally, and the external check gets blocked the same way Jetpack's does.)&lt;/p&gt;

&lt;p&gt;The honest fix for both is allowlisting, not blocking, and it is worth stating clearly because the forum advice often gets this backwards. You allowlist the specific monitor so your security layer stops eating its requests, and you keep the rest of your hardening in place. But allowlisting alone only stops the false alarms. It does nothing about the second, quieter failure, which is the subject of the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going blind: a homepage 200 is not proof your site works
&lt;/h2&gt;

&lt;p&gt;Now flip the failure mode. Jetpack pings your homepage with an HTTP HEAD request and reads the response. ManageWP assesses your site's status from its response code, with an optional single-keyword match on the homepage. In both cases the monitor is asking one question: did the homepage answer with a 200? And an HTTP 200 is not proof that your site works. It is proof that one URL returned a success code.&lt;/p&gt;

&lt;p&gt;Think about what actually breaks on a WordPress site and where it lives. A WooCommerce checkout that throws a fatal error on the payment step. A wp-admin that locks every editor out after a plugin update. A membership area that silently logs users out because a session table filled up. A page that renders a white screen below the fold while the header and footer come through fine. Every one of those can sit behind a homepage that still returns 200. The monitor sees green. Your customers see a dead checkout. This is the general shape of a silent outage, and we cover the broader pattern, including why most uptime tools miss it, in &lt;a href="https://velprove.com/blog/why-uptime-monitors-miss-outages" rel="noopener noreferrer"&gt;why uptime monitors miss real outages&lt;/a&gt; . The Jetpack and ManageWP version is just the most common WordPress instance of it.&lt;/p&gt;

&lt;p&gt;This is not a complaint we are quoting from a forum, because owners rarely file a support ticket that says "my monitor stayed green during an outage" (they never knew the monitor should have caught it). It is an architectural limit you can reason about directly. A monitor that only knows the homepage status code can only ever tell you the homepage answered. It is structurally incapable of telling you that the logged-in dashboard renders, that the checkout completes, or that the members area still authenticates, because it never asks. The optional ManageWP keyword match helps a little, but only on the homepage, and only against a single static string, so a white-screen checkout three clicks deep is still invisible.&lt;/p&gt;

&lt;p&gt;The takeaway is uncomfortable but clean. The cry-wolf problem makes the monitor annoying. The go-blind problem makes it dangerous, because it gives you a false sense of safety. A green pill on a homepage HEAD check is the monitoring equivalent of checking that the front door opens and concluding the whole house is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "switch to another external monitor" does not fix it
&lt;/h2&gt;

&lt;p&gt;The usual advice when Jetpack gets noisy is to switch to a dedicated external uptime monitor. Plenty of articles frame this as "external monitoring is the key," and Automattic staff have themselves recommended moving to a separate service in support threads. Switching off the built-in monitor and onto a dedicated one is reasonable. But if the dedicated monitor you switch to is just another homepage status check, you have not fixed anything. You have moved the same two failure modes to a different logo.&lt;/p&gt;

&lt;p&gt;Walk it through. Both Jetpack and ManageWP already check your site from the outside, so swapping one outside vendor for another that does the same shallow thing changes nothing. A second homepage-200 monitor inherits both problems: it still times out on a slow shared-hosting load, it still gets firewalled if your security layer blocks its probe, and it still goes blind to anything behind a 200. You have changed vendors, not failure modes.&lt;/p&gt;

&lt;p&gt;What actually moves the needle is depth plus robustness. Depth means checking what a user actually does, the login flow, the post-login content, a money path, rather than only whether one URL returns a status code. Robustness means a probe that is harder to false-trip and a configuration that does not page you on a single slow load. If you want to move off a plugin entirely and run your monitoring from outside WordPress, the mechanics of doing that cleanly, without touching wp-content, are in &lt;a href="https://velprove.com/blog/monitor-wordpress-uptime-without-plugins" rel="noopener noreferrer"&gt;monitoring WordPress uptime without a plugin&lt;/a&gt; . The point of this section is narrower: do not let the "just go external" advice convince you that the vendor was the problem. The shallow check was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The durable fix, in plain steps: monitor what a user does
&lt;/h2&gt;

&lt;p&gt;The fix is to stop asking "did the homepage return 200" and start asking "can a real user do the thing they came here to do." That is four moves, and the first one is the differentiator. None of this involves any configuration file or API payload. You build each of these in a wizard, step by step, the same way you would set up a forwarding rule in your email client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move one: a browser login monitor on your own wp-login.php.&lt;/strong&gt; This is the move that catches the most, and it is the one a homepage HEAD request can never replicate. Velprove's browser login monitor opens your own wp-login.php in a real browser, signs in with a dedicated Subscriber-role test account, waits for the dashboard to load, and asserts that a logged-in-only string is actually on the page. If wp-admin locks everyone out after a plugin update, this monitor fails on the next check, because the real browser cannot reach the signed-in state. A status-code probe would have stayed green the whole time. Step one in the wizard: a real browser opens &lt;code&gt;yourdomain.com/wp-login.php&lt;/code&gt;. Step two: it signs in with the test account's username and password. Step three: it asserts that a logged-in-only string (the "Howdy" greeting, the admin bar, a dashboard widget label) is present. If you want the step-by-step setup of a wp-admin login monitor specifically, with screenshots, that lives in &lt;a href="https://velprove.com/blog/monitor-wordpress-login" rel="noopener noreferrer"&gt;how to monitor your wp-admin login&lt;/a&gt; .&lt;/p&gt;

&lt;p&gt;One hard rule on this move, because it is the most common mistake. Point the browser login monitor at &lt;em&gt;your own&lt;/em&gt; &lt;code&gt;wp-login.php&lt;/code&gt; on your own domain, where you control a dedicated low-privilege test user. Do not point it at WordPress.com or Jetpack's own login, which sit behind device verification and email codes a monitor cannot complete (this is covered in the FAQ below). And never wire your real admin credentials into a monitor. Create a throwaway Subscriber account whose only job is to prove the login flow works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move two: a content assertion on the homepage, not just a status code.&lt;/strong&gt; Keep an HTTP monitor on the homepage, but make it assert on a specific string that only renders when the page actually built correctly. The title of your latest post, a footer copyright line with the current year, a product name. A white screen of death often still returns a 200 with an almost-empty body, so a status-only check passes while the page is blank. A body assertion on a real string fails the moment the content stops rendering. The wizard move: add the homepage URL, then add a body-contains assertion on a string you know is always on the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move three: a multi-step monitor for a money path.&lt;/strong&gt; If your site sells something or has a multi-page flow that matters, a multi-step monitor walks a short sequence of requests in order and asserts at each stop. Velprove's free plan covers a multi-step monitor of up to three steps, which is enough to fetch a page, follow it to the next, and assert that the expected content arrives. Each step asserts against a static expected value, a status code or a known string, not a moving target. Velprove runs each step once in sequence; there is no polling or wait-for-condition primitive, so if you need a freshness check, your own endpoint should compute that server-side and the monitor asserts a 200 on it. The wizard move: add step one, add its assertion, add step two, and so on, up to three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move four: spread your HTTP and multi-step monitors across regions.&lt;/strong&gt; A single blocked region is one of the cry-wolf causes from earlier. All five Velprove regions (North America, Europe, United Kingdom, Asia, Oceania) are available on every plan, including free. HTTP and multi-step monitors can be distributed across regions, so running the same homepage assertion from a few different locations tells you whether a failed check is your site falling over or just one regional path getting firewalled or hitting a CDN issue. The browser login monitor runs from one region at a time, so pick the region closest to most of your users for that one. If a probe from a single location trips while the others stay green, you are looking at a one-path problem, not a site-wide outage.&lt;/p&gt;

&lt;p&gt;Those four moves fit inside Velprove's free plan: 10 monitors total, one browser login monitor, a multi-step monitor up to three steps, all five regions, email alerts, no credit card required, and commercial use allowed. The browser login monitor on your own wp-login is the piece that separates this from yet another homepage ping, and it is the piece neither Jetpack nor ManageWP can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with Jetpack or ManageWP once you have real monitoring
&lt;/h2&gt;

&lt;p&gt;Once a deeper monitor is watching the login flow and real content, the built-in monitor has one of two honest jobs left. You can keep it on as a free homepage backstop, a second pair of eyes on the single thing it can actually see, and treat its alerts as low-priority hints rather than pages. Or you can turn it off to stop the noise, especially if the cry-wolf timeouts and the &lt;code&gt;xmlrpc.php&lt;/code&gt; blocks have already trained you to ignore its emails. Both are reasonable. A monitor you ignore is worse than no monitor, so if Jetpack's false alarms have burned your trust, turning it off and leaning on the deeper monitor is the cleaner choice.&lt;/p&gt;

&lt;p&gt;The bigger structural question, whether a WordPress owner should run a built-in plugin monitor at all, which segment each tool fits, and how to choose among the options, is not what this post is for. This post is about why the false reports happen and how to make them stop. The full landscape decision lives in our &lt;a href="https://velprove.com/blog/wordpress-uptime-monitoring-guide-2026" rel="noopener noreferrer"&gt;complete guide to WordPress uptime monitoring&lt;/a&gt; , which walks the whole field including where Jetpack Monitor and ManageWP fit and when each one is enough. If you arrived here trying to decide what to use rather than why the alerts are lying to you, start there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does Jetpack Monitor keep emailing that my site is down when it loads fine for me?
&lt;/h3&gt;

&lt;p&gt;Two causes account for most of these phantom alerts. First, Jetpack flags your site down if it does not answer within about ten seconds, and a busy shared host can blow past that window on a single check while real visitors with warm caches load the page fine. The alert body usually says your site is responding intermittently or extremely slowly and carries an error reference ending in &lt;code&gt;/intermittent&lt;/code&gt;. Second, something on your side blocks the probe before it ever reaches WordPress: a firewall rule, a security plugin, a geoIP block on the United States where the Jetpack servers live, or a blocked &lt;code&gt;xmlrpc.php&lt;/code&gt; file that Jetpack relies on to talk to your site. The page loads for you because your request is not blocked. The probe is. Allowlisting the probe and adding a deeper check that asserts on real content is the durable fix, not turning the timeout knob, because Jetpack does not expose one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does ManageWP show my site as down when it is up?
&lt;/h3&gt;

&lt;p&gt;ManageWP checks your site from an external network, the same way any outside monitor does, and that external probe gets firewalled like any other. A common report is a "Response timeout" that did not receive a response within thirty seconds on a site that is loading fine in a browser, which usually traces back to a security configuration on the host or a security plugin blocking the request before WordPress answers. The first move is to confirm the site really is up from several places, then allowlist the probe so the security layer stops eating it. The deeper move is to monitor what a signed-in user actually does, because a status-code probe will keep reporting a green homepage even when the part of the site your customers use is broken.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a five-minute homepage monitor miss a real outage?
&lt;/h3&gt;

&lt;p&gt;Yes, in two ways. A short outage that opens and closes inside the five-minute gap between checks can be invisible simply because no check landed during it. More importantly, both Jetpack and ManageWP look at whether the homepage returns an HTTP 200, and an HTTP 200 is not proof the site works. A broken checkout, a locked-out wp-admin, a logged-out members area, or a white screen below the fold can all sit behind a 200 response. The homepage answers, the status code is green, and the part of the site that makes you money is dead. A monitor that asserts on real content or drives a real login catches that class of failure. A status-code probe cannot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I block the monitor's IPs or the xmlrpc.php requests to stop the noise?
&lt;/h3&gt;

&lt;p&gt;Blocking is what causes the false alerts in the first place, so blocking harder is the wrong direction. Jetpack Monitor depends on a reachable &lt;code&gt;xmlrpc.php&lt;/code&gt; to talk to your site, and many WordPress owners disable or block &lt;code&gt;xmlrpc.php&lt;/code&gt; for security because it is a common brute-force and amplification target. That is a reasonable security posture, but it breaks Jetpack's connection and produces phantom downtime alerts. The fix is to allowlist the specific monitor's requests rather than turn off your security, or to move the uptime signal to a monitor that does not depend on &lt;code&gt;xmlrpc.php&lt;/code&gt; at all. Pair the allowlist with a monitor that checks depth, a real content assertion or a real login, so you are protected and not paged on noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will a browser login monitor work against WordPress.com or Jetpack's own login?
&lt;/h3&gt;

&lt;p&gt;No. Point a browser login monitor at your own &lt;code&gt;wp-login.php&lt;/code&gt; on your own domain, where you control a dedicated low-privilege test account. It will not work against WordPress.com, the Jetpack dashboard, or any consumer login you do not control, because those sit behind device verification, email codes, and captchas that a monitor cannot complete. That limitation is fine for the WordPress owner, because the surface you actually want to verify is your own login at &lt;code&gt;yourdomain.com/wp-login.php&lt;/code&gt; with a Subscriber-role test user, which is exactly where a browser login monitor belongs.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>uptime</category>
    </item>
    <item>
      <title>Monitor a Supabase App: Auth, RLS, Edge Functions, Realtime</title>
      <dc:creator>velprove</dc:creator>
      <pubDate>Thu, 28 May 2026 14:00:07 +0000</pubDate>
      <link>https://dev.to/velprove/monitor-a-supabase-app-auth-rls-edge-functions-realtime-l50</link>
      <guid>https://dev.to/velprove/monitor-a-supabase-app-auth-rls-edge-functions-realtime-l50</guid>
      <description>&lt;p&gt;&lt;strong&gt;Diagnosis.&lt;/strong&gt; Supabase's status page can be green while your customer's post-login dashboard renders an empty list. RLS-protected reads that fail to a misconfigured policy return an empty array with HTTP 200, which a status-code probe cannot see. A Supabase Auth session can pass its first check, then fail the next one with "Invalid Refresh Token" because refresh tokens are single-use. An Edge Function that boots in 3 ms on a warm isolate can spike into a multi-second tail on the cold path. Realtime can keep its websocket open and stop delivering events. None of those show up on a 200 OK against your app URL. Velprove's free browser login monitor signs into the actual app your users open, the one that reads through Supabase RLS, and a multi-step API monitor re-authenticates against &lt;code&gt;/auth/v1/token&lt;/code&gt; every check and asserts a known row comes back, not just a 200.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Supabase project returns 200 OK. Your RLS reads return [].
&lt;/h2&gt;

&lt;p&gt;PostgREST is the HTTP layer that fronts every Supabase table. When you query it with a publishable key and a user JWT, it consults the row-level security policies on the target table and returns only the rows the policies allow. If the policies are missing or misconfigured, PostgREST does not return a 403 or a 500. It returns an empty JSON array and a 200 status code. A monitor that asserts status_code = 200 sees a happy response. Your customer sees a blank screen.&lt;/p&gt;

&lt;p&gt;From &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase's Row Level Security docs&lt;/a&gt; , the behavior is documented verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"Once you have enabled RLS, no data will be accessible via the API when using a publishable key, until you create policies." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;And, on the default scope:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"RLS must always be enabled on any tables stored in an exposed schema. By default, this is the public schema." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read those sentences together. RLS is supposed to be on. With RLS on and no policy, the API returns nothing. The failure mode is not an HTTP error code. The failure mode is an empty array dressed as a success. This is the load-bearing reason a status-code probe is not enough for a Supabase-backed app: the platform's primary authorization layer fails by returning success. The same family of silent-success failures shows up across cloud platforms in our &lt;a href="https://velprove.com/blog/anatomy-of-a-silent-outage" rel="noopener noreferrer"&gt;silent outages HTTP misses&lt;/a&gt; writeup; the Supabase version is just the cleanest case.&lt;/p&gt;

&lt;p&gt;A migration that drops a policy, a CI step that disables RLS on a table by accident, a refactor that renames the column the policy references: all three produce the same outside-visible shape. 200 OK, empty array, customer dashboard blank. The rest of this post is what to assert on top of the 200 so the empty array trips an alert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feb 12 2026: 3h 42m of us-east-2 dark, every surface at once
&lt;/h2&gt;

&lt;p&gt;On Thursday February 12 2026 at 21:12 UTC, Supabase &lt;a href="https://supabase.com/blog/supabase-incident-on-february-12-2026" rel="noopener noreferrer"&gt;deployed a new internal monitoring service&lt;/a&gt; that took out an entire AWS region. The post-mortem, signed by CEO Paul Copplestone, names the change as the root cause:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"We deployed a new internal monitoring service on February 12th that inadvertently enabled AWS's VPC Block Public Access feature at the regional level in us-east-2. This blocked all internet gateway traffic across every VPC in the region." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The blast radius was the whole region. Verbatim, again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"All Supabase customers with projects hosted in the us-east-2 region were affected." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The incident started at 21:12 UTC and resolved at 00:54 UTC the next morning. 3 hours and 42 minutes. And the affected-surface list is the punchline for this post:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"Postgres databases, Auth, Data APIs, Edge Functions, Storage, Realtime, and any other Supabase service in that region" *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;That list is every Supabase primitive a customer's application reads through. A status-code probe on your own app URL during those 3h 42m would have caught the most visible surface, your web app erroring on its first database read, but it would not have told you which Supabase primitive was failing, whether Auth was issuing tokens, whether Edge Functions were accepting invocations, or whether Realtime channels were open. Each of those needs its own assertion. The pattern is the same pattern we apply to &lt;a href="https://velprove.com/blog/api-health-check-patterns" rel="noopener noreferrer"&gt;what /healthz should return&lt;/a&gt; on any backed-by-something API: assert on the read, not on the handler.&lt;/p&gt;

&lt;p&gt;One incident does not justify a monitoring strategy by itself. The Feb 12 2026 outage is the recent reminder that a BaaS-backed app has a vendor surface area that lives outside your control, and the vendor's own status page is not a substitute for probing your own customer-visible path. The next four H2s are the assertions, one per Supabase primitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth + RLS read in one multi-step (the core wedge)
&lt;/h2&gt;

&lt;p&gt;A multi-step API monitor lets you chain two HTTP calls in sequence and extract a value from the first response into the second request. For a Supabase-backed app, the chain that matters is: get a real user's access token, then use it to read a row the user is supposed to be able to read. If either step fails, your customers cannot use the app. If both steps pass, the auth-and-authorization path is verified from outside, end to end, on every probe interval. The general mechanic is in &lt;a href="https://velprove.com/blog/multi-step-api-monitoring-guide" rel="noopener noreferrer"&gt;the multi-step mechanic&lt;/a&gt; reference; this section is the Supabase-specific shape.&lt;/p&gt;

&lt;p&gt;Step 1 hits the auth endpoint with a dedicated test user's credentials. Step 2 reads from a table protected by RLS, using the access token captured in step 1, and asserts a known row marker appears in the response body. Re-authenticate fresh on every check; do not try to cache a refresh token, because Supabase refresh tokens are single-use (see FAQ #5 for the full reason). The 1-hour default access-token lifetime gives you all the headroom a 5-minute monitor interval needs.&lt;/p&gt;

&lt;p&gt;The shape is small enough to describe in a paragraph. Step 1: &lt;code&gt;POST /auth/v1/token?grant_type=password&lt;/code&gt; with the publishable key in the &lt;code&gt;apikey&lt;/code&gt; header, and the test user's email and password in the body. Assert &lt;code&gt;status_code = 200&lt;/code&gt;, then assert that &lt;code&gt;$.access_token&lt;/code&gt; exists in the JSON response. Capture &lt;code&gt;$.access_token&lt;/code&gt; for the next step. Step 2: &lt;code&gt;GET /rest/v1/canary_rows&lt;/code&gt; with the captured token as &lt;code&gt;Authorization: Bearer&lt;/code&gt; and the same &lt;code&gt;apikey&lt;/code&gt; header. Three assertions: &lt;code&gt;status_code = 200&lt;/code&gt;, &lt;code&gt;body_contains&lt;/code&gt; the &lt;code&gt;user-A-canary&lt;/code&gt; marker, and &lt;code&gt;body_not_contains&lt;/code&gt; the &lt;code&gt;user-B-canary&lt;/code&gt; marker. Screenshot 1 shows the wizard with the chain built end-to-end.&lt;/p&gt;

&lt;p&gt;A few details earn their place. The &lt;code&gt;apikey&lt;/code&gt; header carries the publishable key on both steps; PostgREST requires it on every request and the Auth endpoint requires it on the token call. The &lt;code&gt;Authorization: Bearer&lt;/code&gt; header on step 2 carries the user JWT extracted from step 1. The &lt;code&gt;body_not_contains&lt;/code&gt; assertion is the RLS-enforcement half of the wedge: if a policy got dropped and the read now returns rows owned by user B, the assertion fails. The two assertions together verify both that RLS lets the right rows through AND that RLS blocks the wrong rows.&lt;/p&gt;

&lt;p&gt;Velprove's multi-step monitor runs each step once in sequence with the same 6 assertion types HTTP monitors use: &lt;code&gt;status_code&lt;/code&gt;, &lt;code&gt;body_contains&lt;/code&gt;, &lt;code&gt;body_not_contains&lt;/code&gt;, &lt;code&gt;json_path&lt;/code&gt;, &lt;code&gt;response_time_ms&lt;/code&gt;, and &lt;code&gt;header_contains&lt;/code&gt;. No conditional branching, no per-step retry, no wait-for-condition. That is enough to express the Supabase auth-then-read pattern cleanly, and the simplicity is what keeps the monitor deterministic across thousands of runs.&lt;/p&gt;

&lt;p&gt;The test user posture matters. Provision a dedicated monitoring user with read-only access to a small canary table whose rows carry a stable marker string. Do not point this monitor at a real customer's account, do not give the user write permission, and rotate the password from your monitor's secret vault on the same cadence as your other service credentials. Two test users (one for body_contains, one to source the body_not_contains marker) are the discipline that turns this monitor from a liveness check into an RLS-enforcement check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Functions: cold start as a response-time tail, not a binary
&lt;/h2&gt;

&lt;p&gt;Supabase Edge Functions run on &lt;a href="https://supabase.com/docs/guides/functions/architecture" rel="noopener noreferrer"&gt;V8 isolates inside Deno&lt;/a&gt; , with code packaged in ESZip format for fast boot. The architecture puts cold starts in the single-digit millisecond range under normal conditions; warm invocations return in roughly the same time as the function's own work. The Supabase team shipped a 2025 fix that moved workers performing initial script evaluation onto a dedicated blocking pool, which measurably reduced boot-time spikes in the long tail.&lt;/p&gt;

&lt;p&gt;That is the right shape to monitor as a &lt;code&gt;response_time_ms&lt;/code&gt; assertion rather than a binary up/down signal. Cold starts happen, they recover quickly, and a single slow cold start is not an outage. A sustained shift in the p95 tail is. Set the threshold at 1.5x to 2x your warm p95 measured over a real traffic window, not at a round number pulled from the docs.&lt;/p&gt;

&lt;p&gt;A standalone HTTP monitor against the function URL is the right primitive. The function exposes a public HTTPS endpoint, and a Velprove HTTP monitor can probe it from any of the 5 global regions. Configure three success conditions: &lt;code&gt;status_code = 200&lt;/code&gt;, &lt;code&gt;response_time_ms&lt;/code&gt; under your warm-p95 threshold (1500 ms is a reasonable starting point for most Edge Function workloads), and &lt;code&gt;body_contains&lt;/code&gt; a known string the function emits on the happy path. Screenshot 2 shows the Success Conditions step with all three assertions stacked.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;body_contains&lt;/code&gt; assertion is the part most Edge Function monitors skip and shouldn't. A 200 OK from a function that silently swapped its handler (a deploy that shipped the wrong build, an env var that flipped a feature flag) is still a 200. Asserting on a static string the function's real code path emits turns the same probe into a deploy-correctness check.&lt;/p&gt;

&lt;p&gt;One trap to avoid: do not invoke functions that mutate state on the monitor path. The monitor runs on the configured interval from every region the monitor is configured in, forever. A function that writes a row on each call will accumulate millions of rows over a year. Use a read-only Edge Function path for the canary, or pass a request flag your function honors as a dry-run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser-login on YOUR signed-in surface (not Supabase Studio)
&lt;/h2&gt;

&lt;p&gt;A browser login monitor opens a real browser, navigates to a login page, fills the form with a test user's credentials, waits for the post-login route, and asserts that the page rendered the data it was supposed to render. For a Supabase-backed app, the page that renders post-login is the one that reads through RLS. If RLS is broken or the Data API is down, the page returns 200, renders the chrome, and shows an empty state. The browser login monitor catches that, because it asserts on the data, not on the response code. The general pattern is in our &lt;a href="https://velprove.com/blog/monitor-saas-login-page" rel="noopener noreferrer"&gt;browser login monitor on your signed-in surface&lt;/a&gt; guide; the Supabase-specific detail is below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hard rule: never point this monitor at supabase.com, the Supabase dashboard, or Supabase Studio.&lt;/strong&gt; The target is the customer's own application: the URL your real users open to sign in. Supabase's own UIs sit behind device verification, captchas, and account-level protections the monitor cannot complete. The monitor's job is to verify the path your customer takes through your app, which happens to be backed by Supabase Auth and the Supabase Data APIs.&lt;/p&gt;

&lt;p&gt;The Supabase-distinguishing detail in the assertion: set the monitor's post-login success check to a string that only renders after the dashboard's first RLS-protected read completes. A customer name pulled from the &lt;code&gt;profiles&lt;/code&gt; table. An invoice ID from the &lt;code&gt;invoices&lt;/code&gt; table. The label of a plan the user is actually on. If the RLS policy on that table drops, the page loads but the string never renders, and the monitor fails. For a stronger signal, set the success check to &lt;code&gt;selector_visible&lt;/code&gt; on a DOM element that only renders after the post-login RLS read completes (a row from the user's &lt;code&gt;profiles&lt;/code&gt; table, an invoice ID from the &lt;code&gt;invoices&lt;/code&gt; table). That catches the case where the page renders cached chrome but the user's data layer underneath has gone empty.&lt;/p&gt;

&lt;p&gt;The free plan covers 1 browser login monitor at a 15-minute interval. That is enough for the production signed-in path of a single application. Paid plans add more browser monitors and tighter intervals. The browser monitor is the one assertion in this post that catches a class of failures the API-only monitors cannot: an authenticated page that renders a 200 but is functionally broken because the data layer underneath it returned nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Realtime: probe a customer-side /realtime-health endpoint
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://supabase.com/docs/guides/realtime/architecture" rel="noopener noreferrer"&gt;Supabase Realtime&lt;/a&gt; runs on Elixir with the Phoenix Framework and delivers three primitives over WebSockets: Broadcast, Presence, and Postgres Changes. Postgres Changes adheres to RLS policies on the tables you subscribe to. The whole thing is fast and well-engineered. The whole thing also has no probe surface Velprove can directly assert on, because we have no websocket primitive in any of our monitor types (http, api, multi_step, browser).&lt;/p&gt;

&lt;p&gt;The right response is to push the freshness window into the customer's own infrastructure. Stand up a tiny server-side process that subscribes to the channel you care about, records the timestamp of the last event it received, and exposes an HTTP endpoint that returns 200 or 503 based on how stale that timestamp is. The endpoint owns the freshness logic, and Velprove asserts &lt;code&gt;status_code = 200&lt;/code&gt; on it from outside. This is the same &lt;a href="https://velprove.com/blog/api-health-check-patterns" rel="noopener noreferrer"&gt;/healthz pattern for compound dependencies&lt;/a&gt; : compute the verdict server-side, expose a binary endpoint, probe the binary.&lt;/p&gt;

&lt;p&gt;A minimal implementation in Node with the supabase-js client:&lt;/p&gt;

&lt;p&gt;A few notes on the shape. The endpoint computes the verdict on each request from a server-local timestamp; nothing about the request itself drives the calculation. The Velprove monitor that probes it is the simplest HTTP monitor in this whole post: a &lt;code&gt;GET&lt;/code&gt; against the &lt;code&gt;/realtime-health&lt;/code&gt; URL on a 60-second interval with a single &lt;code&gt;status_code = 200&lt;/code&gt; assertion. No body assertion, no response-time threshold; the customer endpoint already encoded all of those concerns in its 200-versus-503 return.&lt;/p&gt;

&lt;p&gt;That is it. The endpoint owns "what counts as stale," Velprove owns "tell me when it isn't 200," and the customer's real Realtime delivery path is being exercised continuously by the server-side subscriber. If the channel falls silent, the timestamp stops advancing, the endpoint flips to 503, and the next probe pages you.&lt;/p&gt;

&lt;p&gt;Two disciplines matter. First, set the staleness tolerance to the real-world cadence of events on the channel: a 5-minute window on a channel that fires every few seconds catches a real silence quickly; a 5-minute window on a channel that fires once an hour will false-positive constantly. Second, the subscriber process needs its own uptime story; if it crashes the timestamp also stops advancing, which is correct alerting behavior but means you should keep the subscriber simple and run it as part of your normal application deployment, not as a one-off script.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this post is the wrong one
&lt;/h2&gt;

&lt;p&gt;Supabase is one BaaS, and this post is scoped to that surface. If you got here looking for something else, three sibling posts probably fit better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your question is platform-shaped, not BaaS-shaped.&lt;/strong&gt; A Supabase-backed app still has a host that serves its frontend and a runtime that serves its backend. For platform-side monitoring of those hosts, the per-platform guides are &lt;a href="https://velprove.com/blog/monitor-vercel-hosted-site" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt;, &lt;a href="https://velprove.com/blog/monitor-render-hosted-app" rel="noopener noreferrer"&gt;Render&lt;/a&gt;, &lt;a href="https://velprove.com/blog/monitor-railway-app" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;, &lt;a href="https://velprove.com/blog/monitor-cloudflare-workers-pages-site" rel="noopener noreferrer"&gt;Cloudflare Workers + Pages&lt;/a&gt; , and &lt;a href="https://velprove.com/blog/monitor-heroku-app" rel="noopener noreferrer"&gt;Heroku&lt;/a&gt;. Each covers the platform's own failure modes (cold starts, release-phase failures, Eco dyno sleep, regional outages) which are orthogonal to Supabase's.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your question is about choosing between a browser monitor and an HTTP monitor.&lt;/strong&gt; The general rule of thumb is in the &lt;a href="https://velprove.com/blog/browser-monitor-vs-http-monitor-decision-tree" rel="noopener noreferrer"&gt;browser vs HTTP decision tree&lt;/a&gt; . The short version for Supabase: use an HTTP or multi-step monitor for the API surface, and use a browser login monitor for the customer-facing signed-in path that reads through RLS. Both, not either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you are not sure which Velprove plan covers this.&lt;/strong&gt; The four-monitor Supabase set in this post (multi-step auth+RLS, HTTP Edge Function, HTTP Realtime freshness, browser login) fits inside the free plan: 10 monitors total, 1 browser login monitor, multi-step up to 3 steps. If you need more browser monitors, multi-step chains longer than 3 steps, Slack/Discord/Teams/Webhooks delivery (Starter), or PagerDuty (Pro), see &lt;a href="https://velprove.com/blog/which-velprove-plan-for-your-site" rel="noopener noreferrer"&gt;which Velprove plan&lt;/a&gt; fits your shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I assert that RLS is actually enforced and not silently disabled?
&lt;/h3&gt;

&lt;p&gt;Provision two low-privilege test users in your Supabase project, A and B, with disjoint row ownership: a row only A can read carries a marker string &lt;code&gt;user-A-canary&lt;/code&gt;, and a row only B can read carries &lt;code&gt;user-B-canary&lt;/code&gt;. Run the Auth + RLS multi-step monitor as user A. On the RLS read step, assert two conditions in this order: &lt;code&gt;body_contains&lt;/code&gt; the &lt;code&gt;user-A-canary&lt;/code&gt; marker, and &lt;code&gt;body_not_contains&lt;/code&gt; the &lt;code&gt;user-B-canary&lt;/code&gt; marker. If RLS is enforced, both pass. If RLS was disabled on the table (or the policy got dropped during a migration), the read returns both rows, the second assertion fails, and Velprove pages you. Velprove cannot tell you that the policy is misconfigured, only that the expected row scope changed. That symptom-not-cause framing is enough to put a human on the database within minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Velprove monitor a Supabase Edge Function cold start?
&lt;/h3&gt;

&lt;p&gt;Yes. Create a Velprove HTTP monitor against your function URL and add a &lt;code&gt;response_time_ms&lt;/code&gt; assertion at roughly 1.5x to 2x your warm p95. Edge Functions run on V8 isolates with ESZip cold starts in the single-digit-millisecond range under normal conditions, but boot-time spikes still happen on first invocation after idle. Setting the threshold above warm p95 catches the long tail without paging on every warm request.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if my Supabase Realtime channel stops delivering events?
&lt;/h3&gt;

&lt;p&gt;Velprove has no websocket primitive, so the realtime channel itself is not directly probeable. Move the freshness window into your own infrastructure: expose a &lt;code&gt;/realtime-health&lt;/code&gt; endpoint that subscribes to the channel server-side, records the timestamp of the last delivered event, and returns 200 when the gap is below your tolerance or 503 when it exceeds it. A Velprove HTTP monitor asserts &lt;code&gt;status_code = 200&lt;/code&gt; on that endpoint on your normal interval. The endpoint owns the freshness logic, and Velprove owns the alerting and the global probe origins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should the multi-step monitor use the service-role key or the anon/publishable key?
&lt;/h3&gt;

&lt;p&gt;The publishable (anon) key plus a real test-user JWT obtained at the first step. The service-role key bypasses RLS by design. A monitor authenticated with the service-role key will return rows whether or not the policy enforces correctly, so the entire RLS-enforcement wedge collapses to noise. Use the publishable key in the &lt;code&gt;apikey&lt;/code&gt; header and the test user's &lt;code&gt;access_token&lt;/code&gt; in the &lt;code&gt;Authorization: Bearer&lt;/code&gt; header. That is the same posture your real customer's browser uses, which is the posture you want to be monitoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my Supabase Auth multi-step fail every other check with "Invalid Refresh Token"?
&lt;/h3&gt;

&lt;p&gt;Supabase refresh tokens are single-use. From the &lt;a href="https://supabase.com/docs/guides/auth/sessions" rel="noopener noreferrer"&gt;Sessions docs&lt;/a&gt; : a refresh token can only be used once to exchange for a new access-and-refresh-token pair. If your monitor caches a refresh token between checks and tries to refresh on the second run, the first refresh consumed the token and the second call gets "Invalid Refresh Token." The fix is to not refresh at all. Call &lt;code&gt;POST /auth/v1/token?grant_type=password&lt;/code&gt; fresh on every check, get a brand new access token, and discard everything when the check completes. A 5-minute monitor interval against a 1-hour access-token lifetime means you never approach expiry anyway, and the refresh-token consumption problem stops existing.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>uptime</category>
    </item>
    <item>
      <title>Can a Browser Monitor Sign In With OAuth, SSO, or a Passkey?</title>
      <dc:creator>velprove</dc:creator>
      <pubDate>Wed, 27 May 2026 14:00:02 +0000</pubDate>
      <link>https://dev.to/velprove/can-a-browser-monitor-sign-in-with-oauth-sso-or-a-passkey-5430</link>
      <guid>https://dev.to/velprove/can-a-browser-monitor-sign-in-with-oauth-sso-or-a-passkey-5430</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; No. A form-fill browser login monitor cannot complete an OAuth redirect, a SAML or OIDC SSO bounce, a passkey ceremony, an MFA challenge, or a magic link. Stop trying to make it. Monitor the identity provider's token endpoint with a multi-step API monitor, assert one protected route with the access token, and expose a small canary route in your own app that an HTTP monitor can poll. That combination catches Entra and Okta outages in under five minutes. If your login actually is email and password on your own domain, the browser login monitor is the right tool and we already wrote that post: see how to &lt;a href="https://velprove.com/blog/monitor-saas-login-page" rel="noopener noreferrer"&gt;monitor a SaaS login that IS email and password&lt;/a&gt; . This post is the sibling for everything else. Built for &lt;a href="https://velprove.com/for/saas" rel="noopener noreferrer"&gt;SaaS application monitoring&lt;/a&gt; teams whose login is anything but a single form. &lt;a href="https://velprove.com/signup" rel="noopener noreferrer"&gt;Start for free.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Microsoft 365 lost SSO and MFA on October 8, 2025. Status-only monitors stayed green.
&lt;/h2&gt;

&lt;p&gt;On October 8, 2025, Microsoft posted incident MO1168102 against Microsoft 365. The impact list named Microsoft Teams, Exchange Online, the Microsoft 365 admin center, Microsoft Entra SSO authentication, and MFA. Microsoft's own root-cause language was * "a portion of directory operations infrastructure which became imbalanced during a period of high traffic volume and caused authorization failures" * ( &lt;a href="https://www.bleepingcomputer.com/news/microsoft/microsoft-365-outage-blocks-access-to-teams-exchange-online/" rel="noopener noreferrer"&gt;BleepingComputer coverage, MO1168102&lt;/a&gt; ). Translation: the part of Entra that issues authorization decisions stopped issuing them, while everything else looked fine from the outside.&lt;/p&gt;

&lt;p&gt;Status-only monitors pointed at portal.office.com kept returning 200 OK while the IdP path was failing. The portal HTML rendered. The marketing chrome loaded. The CSS came back. Real users hit sign-in and got error pages, because Entra refused to issue a token they could use. Every team whose monitoring stopped at "the host answered" learned the same lesson at the same time. This is the same defect class we walk through in &lt;a href="https://velprove.com/blog/why-uptime-monitors-miss-outages" rel="noopener noreferrer"&gt;five outage classes standard monitoring misses&lt;/a&gt; .&lt;/p&gt;

&lt;p&gt;The honest defense against an IdP-flavored outage is not a better form-fill primitive. It is monitoring the identity provider as a third-party dependency you do not own (we cover the broader pattern in &lt;a href="https://velprove.com/blog/monitor-third-party-dependency-you-dont-own" rel="noopener noreferrer"&gt;treat your IdP as a third-party dependency&lt;/a&gt; ), plus a canary route in your app that says out loud whether authentication just round-tripped. Both are cheap. Both are unambiguous. Neither is a form-fill browser monitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a browser login monitor actually drives
&lt;/h2&gt;

&lt;p&gt;Velprove's browser login monitor is a form-fill primitive. Every browser login monitor has exactly one &lt;code&gt;loginUrl&lt;/code&gt;, exactly two credentials stored in &lt;code&gt;check_secrets&lt;/code&gt; ( &lt;code&gt;BROWSER_USERNAME&lt;/code&gt; and &lt;code&gt;BROWSER_PASSWORD&lt;/code&gt;), three optional CSS selectors for the username field, the password field, and the submit button (auto-detected when blank), and one &lt;code&gt;successIndicator&lt;/code&gt; chosen from &lt;code&gt;url_pattern&lt;/code&gt;, &lt;code&gt;text_present&lt;/code&gt;, or &lt;code&gt;selector_visible&lt;/code&gt;. That is the entire shape.&lt;/p&gt;

&lt;p&gt;Every check loads the URL, locates the two fields, types the credentials, clicks submit, waits for navigation, and checks the success indicator against the rendered DOM. That is exactly what a real customer signing in with email and password looks like, which is why it works for that case. It is also why it fails for everything else: there is no second URL, no token capture, no third-party redirect handling, no second factor input, no authenticator integration, no inbox reader.&lt;/p&gt;

&lt;p&gt;Velprove ships four monitor types in total: Browser login, HTTP, API, and Multi-step API. When the login flow is not a single form, the work moves out of the browser primitive and into the multi-step API plus HTTP primitives. The rest of this post is the recipe for that move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five login patterns a form-fill browser monitor cannot drive
&lt;/h2&gt;

&lt;p&gt;Five common login patterns push the work outside what a form-fill primitive can do. The honest answer in each case is the same: do not bend the browser monitor into the wrong shape. Use the right primitive for the actual auth flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: OAuth redirect (authorization code flow)
&lt;/h3&gt;

&lt;p&gt;OAuth's authorization code flow ( &lt;a href="https://oauth.net/2/" rel="noopener noreferrer"&gt;OAuth 2.0 spec&lt;/a&gt; ) bounces the user to a third-party authorization server, asks for consent, redirects back with a code, and exchanges the code for an access token at the token endpoint. The browser login monitor has one &lt;code&gt;loginUrl&lt;/code&gt;; it cannot follow the bounce to the provider, click the consent button, and ride the redirect back with the code in the query string.&lt;/p&gt;

&lt;p&gt;Right primitive: a multi-step API monitor against the token endpoint using &lt;code&gt;client_credentials&lt;/code&gt; for service-to-service cases, or &lt;code&gt;refresh_token&lt;/code&gt; for a long-lived refresh token you minted out of band for the synthetic test identity. The browser chrome is not what is interesting; the token round trip is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: SSO via SAML or OIDC IdP
&lt;/h3&gt;

&lt;p&gt;SAML 2.0 ( &lt;a href="https://en.wikipedia.org/wiki/SAML_2.0" rel="noopener noreferrer"&gt;SAML 2.0&lt;/a&gt; ) bounces the user from the service provider to the identity provider, posts a signed assertion back, and ends with a session in the SP. OpenID Connect ( &lt;a href="https://openid.net/connect/" rel="noopener noreferrer"&gt;OpenID Connect&lt;/a&gt; ) is the modern equivalent over OAuth. Neither survives a single form fill. SAML ECP (Enhanced Client or Proxy) exists, but it requires the IdP to enable an ECP profile, which most enterprise deployments deliberately disable. Do not assume ECP. Do not pitch ECP to a customer who has not turned it on.&lt;/p&gt;

&lt;p&gt;Right primitive: OIDC &lt;code&gt;client_credentials&lt;/code&gt; against the IdP's token endpoint, plus a canary route in your app that proves the SP-side session check still works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 3: Passkey or WebAuthn
&lt;/h3&gt;

&lt;p&gt;WebAuthn ( &lt;a href="https://www.w3.org/TR/webauthn-2/" rel="noopener noreferrer"&gt;W3C WebAuthn Level 2&lt;/a&gt; ) requires a real authenticator: a hardware security key, a phone biometric, or a platform credential signed by the OS. Chrome DevTools and Playwright support a virtual authenticator for testing an app you control. Velprove's browser login monitor primitive does not wire up a virtual authenticator. Do not assume it does.&lt;/p&gt;

&lt;p&gt;Right primitive: monitor the IdP's OIDC discovery endpoint and token endpoint with HTTP and multi-step API monitors. Reserve the browser login monitor for the email-and-password flow on your own login page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 4: MFA challenge (TOTP, push, SMS)
&lt;/h3&gt;

&lt;p&gt;Time-based one-time passwords need a shared secret and a clock. A push notification needs a phone and a human. An SMS needs a carrier path Velprove will not touch. None of these fit into the single-form-fill shape. Provision a synthetic test identity with MFA disabled and the lowest scope you can grant. Monitor the IdP and the protected route, not the human ceremony.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 5: Magic link
&lt;/h3&gt;

&lt;p&gt;Magic-link sign-in posts an email to a server, the server emails a token, the user clicks the token, the server creates a session. Velprove does not read inboxes. The honest workaround is to assert the issuance call returns 200 with a multi-step API monitor, then assert a protected-route response using a token your app generates server-side for the synthetic test identity. If the link generator falls over, the issuance call fails. If the session backend falls over, the protected route fails. Two signals, no inbox required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workaround pattern: monitor the auth API, then assert the canary route
&lt;/h2&gt;

&lt;p&gt;Seven steps inside this section, plus an eighth in the conclusion. About twenty minutes of work the first time, five minutes for each subsequent app. The HowTo schema embedded in this page mirrors the steps below verbatim.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pick the right primitive: browser, multi-step API, or HTTP
&lt;/h3&gt;

&lt;p&gt;Velprove has four monitor types: Browser login, HTTP, API, and Multi-step API. Pick the browser login monitor only when your sign-in page is a real email-and-password form on your own domain (see the sibling post for that recipe). For OAuth, SAML, passkey, MFA, or magic-link auth, the browser primitive is the wrong tool. Reach for the multi-step API monitor first, plus an HTTP monitor for the canary route. We unpack the choice in detail in &lt;a href="https://velprove.com/blog/browser-monitor-vs-http-monitor-decision-tree" rel="noopener noreferrer"&gt;the seven-question decision tree on browser vs HTTP&lt;/a&gt; .&lt;/p&gt;

&lt;h3&gt;
  
  
  Provision a dedicated test identity (no MFA, lowest scope)
&lt;/h3&gt;

&lt;p&gt;Create a synthetic user in your identity provider. Smallest scope you can grant. MFA disabled (because the monitor cannot complete it). No real customer data behind it. No admin permissions. If the credentials leak, the blast radius is one inert account that can read nothing interesting. The safest approach is always a purpose-built test identity, never a real admin or a real customer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store credentials in Velprove's encrypted fields
&lt;/h3&gt;

&lt;p&gt;Velprove encrypts the multi-step monitor's request body and headers at rest. Paste the &lt;code&gt;client_id&lt;/code&gt;, &lt;code&gt;client_secret&lt;/code&gt;, &lt;code&gt;username&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;, or &lt;code&gt;refresh_token&lt;/code&gt; directly into the body or header fields of the multi-step monitor; the worker decrypts the request server-side before issuing it to your IdP. Browser login monitors use a dedicated &lt;code&gt;check_secrets&lt;/code&gt; store keyed by name (&lt;code&gt;BROWSER_USERNAME&lt;/code&gt; and &lt;code&gt;BROWSER_PASSWORD&lt;/code&gt;) for the same encryption guarantee with a different shape. Plaintext secrets never sit in the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build the multi-step API monitor against the token-grant endpoint
&lt;/h3&gt;

&lt;p&gt;Step 1 of the multi-step monitor POSTs to your IdP's token endpoint with the grant your test identity is configured for. For service-to-service, the right grant is &lt;code&gt;client_credentials&lt;/code&gt;. For an offline-issued user token, use &lt;code&gt;refresh_token&lt;/code&gt;. ROPC is the last resort and we unpack why in the next H2. Assert &lt;code&gt;status_code&lt;/code&gt; is 200 and &lt;code&gt;json_path&lt;/code&gt; on &lt;code&gt;$.access_token&lt;/code&gt; is present. This is exactly the pattern we unpack in &lt;a href="https://velprove.com/blog/multi-step-api-monitoring-guide" rel="noopener noreferrer"&gt;multi-step API monitoring with a token-grant first step&lt;/a&gt; . Velprove's multi-step API monitor exposes exactly six assertion types: &lt;code&gt;status_code&lt;/code&gt;, &lt;code&gt;body_contains&lt;/code&gt;, &lt;code&gt;body_not_contains&lt;/code&gt;, &lt;code&gt;json_path&lt;/code&gt;, &lt;code&gt;response_time_ms&lt;/code&gt;, and &lt;code&gt;header_contains&lt;/code&gt;. Each step runs once. There is no polling, no retry-until, no wait-for-condition, no "within N seconds" freshness primitive. Snapshot per interval, asserted against the response body the IdP just sent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract the access token and call one protected route
&lt;/h3&gt;

&lt;p&gt;Step 2 of the multi-step monitor uses the access token captured from step 1 as a &lt;code&gt;Bearer&lt;/code&gt; header on the &lt;code&gt;Authorization&lt;/code&gt; request header, and calls one protected API route that exercises real authentication. Assert &lt;code&gt;status_code&lt;/code&gt; is 200 and &lt;code&gt;body_contains&lt;/code&gt; a value only an authenticated caller can see (your synthetic identity's email, a known role name, an account ID). This is the assertion that proves the IdP issued a token your API actually accepted, not just that the IdP returned a JSON blob. This pattern works equally well for &lt;a href="https://velprove.com/for/api" rel="noopener noreferrer"&gt;API uptime monitoring for OAuth-protected endpoints&lt;/a&gt; where the API itself is the surface you care about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ship a /healthz/authed canary route in your own app
&lt;/h3&gt;

&lt;p&gt;The multi-step monitor proves the IdP is up and the token round trip works from outside. The canary route proves the same thing from inside your own app, which is the perspective that matches your real customers' experience. Add an unauthenticated endpoint, conventionally &lt;code&gt;/healthz/authed&lt;/code&gt;, that does the real work server-side: take a server-held service-account credential, exchange it for a token against the IdP, call one internal protected handler, and return 200 if the round trip succeeded or 503 if any step failed. The route is unauthenticated from the outside (no secrets sent over the wire) but its 200 is a real signal. We unpack the broader pattern in &lt;a href="https://velprove.com/blog/api-health-check-patterns" rel="noopener noreferrer"&gt;expose an unauthenticated /healthz that proxies the auth state&lt;/a&gt; .&lt;/p&gt;

&lt;h3&gt;
  
  
  Point a Velprove HTTP monitor at the canary route
&lt;/h3&gt;

&lt;p&gt;Add a Velprove HTTP monitor that GETs &lt;code&gt;/healthz/authed&lt;/code&gt; and asserts &lt;code&gt;status_code&lt;/code&gt; is 200 plus a &lt;code&gt;body_contains&lt;/code&gt; string the route returns when the auth round trip succeeded (the literal string &lt;code&gt;"auth: ok"&lt;/code&gt; is plenty; anything stable and non-blank works). This is the second pair of eyes that catches IdP outages your multi-step monitor cannot reach, because the outage is between your app server and the IdP, not between the Velprove worker and the IdP.&lt;/p&gt;

&lt;h2&gt;
  
  
  If your app uses Clerk: the +clerk_test pattern
&lt;/h2&gt;

&lt;p&gt;If your app uses Clerk, this is the cleanest pattern. Clerk supports a deterministic test-mode flow that fits Velprove's browser login primitive exactly. Per Clerk's testing documentation ( &lt;a href="https://clerk.com/docs/guides/development/testing/test-emails-and-phones" rel="noopener noreferrer"&gt;test emails and phones&lt;/a&gt; ): "Any email with the &lt;code&gt;+clerk_test&lt;/code&gt; subaddress is a test email address. No emails will be sent, and they can be verified with the code &lt;code&gt;424242&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;Provision one synthetic test user with an email like &lt;code&gt;monitor+clerk_test@yourapp.com&lt;/code&gt;. Point a Velprove browser login monitor at your Clerk-hosted sign-in URL. The username field gets the &lt;code&gt;+clerk_test&lt;/code&gt; email; the password field gets the fixed verification code &lt;code&gt;424242&lt;/code&gt;. Choose a &lt;code&gt;text_present&lt;/code&gt; success indicator that only renders for signed-in users (a known piece of dashboard chrome, the user's name in the navbar, a logout button). The monitor runs every fifteen minutes from the region of your choice on the free plan.&lt;/p&gt;

&lt;p&gt;One caveat directly from Clerk's documentation: "Every development instance has &lt;strong&gt;test mode&lt;/strong&gt; enabled by default. If you need to use &lt;strong&gt;test mode&lt;/strong&gt; on a production instance, you can enable it in the Clerk Dashboard. However, this is highly discouraged." Translation: prefer pointing this monitor at a staging environment, not at production. Production stays clean; the staging monitor proves the auth path works end to end. We are documenting the pattern, not shipping a case study; if you want a worked staging-environment example, the Clerk docs page above is the canonical source.&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth-protected APIs: client credentials grant is your friend, ROPC is the last resort
&lt;/h2&gt;

&lt;p&gt;Two grants matter for monitoring an OAuth-protected API. Pick the first one you can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth 2.0 client_credentials grant.&lt;/strong&gt; Designed for service-to-service auth. No user in the loop. You exchange a &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt; for an access token. Store both in Velprove check secrets. The monitor never touches a real user account. This is the right grant for almost every Velprove multi-step API monitor that authenticates against an IdP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource Owner Password Credentials (ROPC) grant.&lt;/strong&gt; The user's username and password go straight to the token endpoint. Older, simpler, and actively discouraged by every modern IdP. Microsoft's own documentation on the Entra ROPC flow ( &lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth-ropc" rel="noopener noreferrer"&gt;Microsoft Entra ROPC documentation&lt;/a&gt; ) is unusually direct:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Microsoft recommends you do &lt;em&gt;not&lt;/em&gt; use the ROPC flow; it's incompatible with multifactor authentication (MFA). In most scenarios, more secure alternatives are available and recommended. This flow requires a very high degree of trust in the application, and carries risks that aren't present in other flows. You should only use this flow when more secure flows aren't viable." "As MFA becomes more prevalent, some Microsoft web APIs will only accept access tokens if they have passed MFA requirements. Applications and test rigs relying on ROPC will be locked out."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That second paragraph is the one that matters for monitoring. As more Microsoft APIs gate on MFA-enforced tokens, an Entra ROPC monitor will quietly stop working when the API behind it tightens its conditional access policy. Plan for ROPC to be a temporary workaround, not a long-term monitoring strategy on Entra.&lt;/p&gt;

&lt;p&gt;Other IdPs land in different places. Amazon Cognito supports username-and-password auth via &lt;code&gt;ADMIN_USER_PASSWORD_AUTH&lt;/code&gt; on the admin API (note: the modern flow is &lt;code&gt;ADMIN_USER_PASSWORD_AUTH&lt;/code&gt;, not the older &lt;code&gt;ADMIN_NO_SRP_AUTH&lt;/code&gt;). Google has no public ROPG-equivalent on its OAuth endpoints; service accounts plus the &lt;code&gt;client_credentials&lt;/code&gt; grant against a Google Workspace domain is the supported path. Auth0 supports ROPC behind a tenant setting that is off by default. Okta supports the resource-owner password flow on tenants that explicitly enable it. In every case, the recommended order is the same: try &lt;code&gt;client_credentials&lt;/code&gt; first, fall back to &lt;code&gt;refresh_token&lt;/code&gt; with a long-lived refresh token minted out of band, and only reach for ROPC when both of those are unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use HTTP or multi-step API instead of a browser login monitor
&lt;/h2&gt;

&lt;p&gt;Three short questions decide it. The honest decision rubric, in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is your sign-in page an email-and-password form on a URL you own?&lt;/strong&gt; If yes, use a browser login monitor. That is exactly its shape and it will catch the post-200-OK assertion failures status-only checks miss. We covered this case in the sibling post on &lt;a href="https://velprove.com/blog/monitor-saas-login-page" rel="noopener noreferrer"&gt;monitor a SaaS login that IS email and password&lt;/a&gt; . &lt;strong&gt;Is your sign-in a redirect to a third-party IdP, a passkey ceremony, an MFA challenge, or a magic link?&lt;/strong&gt; If yes, the browser login monitor is the wrong primitive. Use a multi-step API monitor against the IdP's token endpoint plus an HTTP monitor against an unauthenticated canary route in your own app. &lt;strong&gt;Is your API the surface you actually care about?&lt;/strong&gt; If yes, skip the browser layer entirely. A multi-step API monitor that does &lt;code&gt;client_credentials&lt;/code&gt; against the IdP and calls one protected route covers the case in two steps.&lt;/p&gt;

&lt;p&gt;We unpack the full version of this rubric, with the seven branches that matter, in &lt;a href="https://velprove.com/blog/browser-monitor-vs-http-monitor-decision-tree" rel="noopener noreferrer"&gt;the seven-question decision tree on browser vs HTTP&lt;/a&gt; . The short version above is enough for ninety percent of the choices teams actually face.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitor the IdP as a third-party dependency
&lt;/h2&gt;

&lt;p&gt;Once you accept that Entra, Okta, Auth0, Clerk, or Google is on the critical path for your sign-in, the right framing is to &lt;a href="https://velprove.com/blog/monitor-third-party-dependency-you-dont-own" rel="noopener noreferrer"&gt;treat your IdP as a third-party dependency&lt;/a&gt; and monitor it the way you monitor any other vendor in your request path. Two endpoints carry most of the signal.&lt;/p&gt;

&lt;p&gt;The OpenID Connect discovery endpoint, conventionally &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt;, is a public JSON document every OIDC IdP exposes. Velprove can hit it with an HTTP monitor, assert &lt;code&gt;status_code&lt;/code&gt; 200 and &lt;code&gt;body_contains&lt;/code&gt; on the literal &lt;code&gt;"token_endpoint"&lt;/code&gt; field name, and you have a no-credentials smoke test that the IdP is reachable and serving configuration. The token endpoint itself, exercised by the multi-step monitor described above, is the second signal. When Entra MO1168102 reproduced in October 2025, the discovery endpoint kept responding while the token endpoint returned authorization failures. Two monitors, one signal each, no overlap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can Velprove's browser login monitor complete an OAuth redirect to Google or Microsoft?
&lt;/h3&gt;

&lt;p&gt;No. Velprove's browser login monitor is a form-fill primitive. It loads one login URL, types a username and a password into two fields, clicks a submit button, and asserts one of three success indicators (a URL pattern, a piece of text, or a CSS selector). It cannot follow an OAuth redirect to a third-party identity provider, handle the consent screen, or complete the authorization code exchange. Use the multi-step API monitor against the IdP's token endpoint instead, plus an unauthenticated canary route in your own app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a browser login monitor sign in via SAML SSO or Okta?
&lt;/h3&gt;

&lt;p&gt;No. SAML SSO bounces the user through the identity provider, posts a signed assertion back to the service provider, and ends with a session in the SP. A form-fill browser monitor with one loginUrl and two credential fields cannot drive that bounce chain. The clean workaround is OIDC &lt;code&gt;client_credentials&lt;/code&gt; against the IdP's token endpoint plus a canary route in your app that exercises the SP-side session check.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about passkey or WebAuthn login?
&lt;/h3&gt;

&lt;p&gt;No. WebAuthn requires a real authenticator (a security key, a phone biometric, or a platform credential). A form-fill browser monitor has no authenticator attached and no way to attach a virtual one in this primitive. Monitor the IdP's discovery and token endpoints with HTTP and multi-step API monitors instead. Reserve browser login monitors for the email-and-password flow on your own login page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Velprove handle TOTP, push, or SMS MFA?
&lt;/h3&gt;

&lt;p&gt;No. The browser login monitor has one login URL, two credentials, and three optional selectors. It cannot read a TOTP code from an authenticator app, approve a push notification on a phone, or receive an SMS. Use a dedicated synthetic test identity with MFA disabled and the lowest scope you can grant. Monitor the IdP and the protected route, not the human MFA ceremony.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I monitor a Clerk-protected app?
&lt;/h3&gt;

&lt;p&gt;If your app uses Clerk, the cleanest pattern is the &lt;code&gt;+clerk_test&lt;/code&gt; subaddress in development mode. Clerk's documentation states that any email with the &lt;code&gt;+clerk_test&lt;/code&gt; subaddress is a test email address, no emails are sent, and they can be verified with the code &lt;code&gt;424242&lt;/code&gt;. Create a synthetic test user with a &lt;code&gt;+clerk_test&lt;/code&gt; email, point a Velprove browser login monitor at your Clerk-hosted sign-in page, fill the email and the fixed &lt;code&gt;424242&lt;/code&gt; verification code, and assert a post-login element. Clerk also notes that test mode is highly discouraged on production instances, so prefer running this pattern against a staging environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about magic-link login?
&lt;/h3&gt;

&lt;p&gt;No. Magic-link auth requires reading an email inbox to extract the token, which Velprove does not do. The workaround is the same as for SSO: monitor the magic-link issuance endpoint with a multi-step API monitor that asserts the issuance call returns 200, then assert the protected-route response with a token your app generates server-side for the synthetic test identity. If the magic-link generator falls over, the issuance call fails. If the session backend falls over, the protected route fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the canary-route workaround a real monitor or a hack?
&lt;/h3&gt;

&lt;p&gt;It is a real monitor and it is also a pattern your own application code controls. A &lt;code&gt;/healthz/authed&lt;/code&gt; route exposed by your app that round-trips against your IdP using a server-side service account is a defensible signal: if the route returns 200 the auth backend is reachable and a token round trip just worked; if the route returns 503 something on the auth path broke. Velprove polls the route with an HTTP monitor and asserts the body. The route does the real auth work in your own infrastructure where you can keep secrets and run logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I monitor an OAuth-protected API without exposing user credentials?
&lt;/h3&gt;

&lt;p&gt;Use the OAuth 2.0 &lt;code&gt;client_credentials&lt;/code&gt; grant if your API supports it. The grant is designed for service-to-service auth: you exchange a &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt; for an access token with no user in the loop. Store both in Velprove &lt;code&gt;check_secrets&lt;/code&gt;, build a multi-step API monitor where step 1 fetches the token and step 2 calls a protected route with the Bearer header, and you have a credential-free monitor that does not touch a real user account. If &lt;code&gt;client_credentials&lt;/code&gt; is not available, the Resource Owner Password Credentials grant works but Microsoft and most modern IdPs actively discourage it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this connects
&lt;/h2&gt;

&lt;p&gt;Login is the single most expensive blind spot in commercial monitoring, and it splits cleanly in two. If your sign-in is a real email-and-password form on a URL you own, the browser login monitor is the right primitive and we wrote the recipe in &lt;a href="https://velprove.com/blog/monitor-saas-login-page" rel="noopener noreferrer"&gt;monitor a SaaS login that IS email and password&lt;/a&gt; . If your sign-in is anything else (OAuth, SAML SSO, OIDC, passkey, MFA, magic link), the browser primitive is the wrong tool and the workaround above is the right one: multi-step API monitor against the IdP token endpoint, HTTP monitor against an unauthenticated canary route in your own app, both signed by a synthetic test identity that lives behind nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wire the alert to the channel that wakes the right person
&lt;/h3&gt;

&lt;p&gt;Velprove ships email alerts on every plan including free. Slack, Discord, Teams, and outbound webhooks unlock on Starter. PagerDuty unlocks on Pro. Pick the channel the on-call actually reads. An alert that lands in a muted Slack channel is worse than no alert, because it teaches the team to trust the silence.&lt;/p&gt;

&lt;p&gt;Free plan, your choice of five regions, browser login monitor every fifteen minutes, multi-step API and HTTP monitors at five-minute minimums, commercial use explicitly allowed, no credit card. &lt;a href="https://velprove.com/signup" rel="noopener noreferrer"&gt;Start for free. Monitor the login your customers actually use.&lt;/a&gt; If your stack is SaaS-shaped, the &lt;a href="https://velprove.com/for/saas" rel="noopener noreferrer"&gt;SaaS application monitoring&lt;/a&gt; page is the right next read; if the surface you care about is an OAuth-protected API, &lt;a href="https://velprove.com/for/api" rel="noopener noreferrer"&gt;API uptime monitoring for OAuth-protected endpoints&lt;/a&gt; covers the same pattern from the API side.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>uptime</category>
    </item>
    <item>
      <title>WHMCS Does Not Retry Failed Provisioning. Here Is How to Catch the Silent Order Chain.</title>
      <dc:creator>velprove</dc:creator>
      <pubDate>Tue, 26 May 2026 14:00:05 +0000</pubDate>
      <link>https://dev.to/velprove/whmcs-does-not-retry-failed-provisioning-here-is-how-to-catch-the-silent-order-chain-3gla</link>
      <guid>https://dev.to/velprove/whmcs-does-not-retry-failed-provisioning-here-is-how-to-catch-the-silent-order-chain-3gla</guid>
      <description>&lt;p&gt;&lt;strong&gt;The mechanic:&lt;/strong&gt; WHMCS does not automatically retry failed module actions. When the upstream cPanel, Plesk, or SolusVM module returns an error during account creation, WHMCS quietly drops the failure into its Module Queue and waits for you to notice. The customer paid the invoice. The order shows Active. The portal login works. The hosting account does not exist. A browser login monitor on &lt;code&gt;clientarea.php&lt;/code&gt; (the &lt;a href="https://velprove.com/blog/monitor-whmcs-portal" rel="noopener noreferrer"&gt;WHMCS portal monitor we covered previously&lt;/a&gt; ) will not catch this by design. What does catch it: a single API monitor that hits &lt;code&gt;GetModuleQueue&lt;/code&gt; and asserts the queue is empty, plus an optional 3-step API chain that simulates the full order, accept-order, and provisioning verification path. Both recipes fit the Velprove free plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHMCS does not retry failed provisioning. Your customer finds out before you do.
&lt;/h2&gt;

&lt;p&gt;If you have already set up the &lt;a href="https://velprove.com/blog/monitor-whmcs-portal" rel="noopener noreferrer"&gt;browser login monitor on your WHMCS client portal&lt;/a&gt; , this post covers what that monitor cannot see by design. The browser login monitor confirms &lt;code&gt;clientarea.php&lt;/code&gt; renders and accepts credentials. It tells you nothing about whether a customer who just placed an order has a working hosting account waiting for them.&lt;/p&gt;

&lt;p&gt;That gap is where WHMCS silent failures live. The order chain (AddOrder, then AcceptOrder, then the underlying provisioning module call against your cPanel, Plesk, or SolusVM box) runs after the login succeeds. When the upstream module fails, WHMCS does not retry. It drops the failure into the Module Queue and waits for you to look at the admin dashboard. The customer finds out before you do, usually by email, usually after they have already tried to use the service that does not exist.&lt;/p&gt;

&lt;p&gt;The WHMCS docs are explicit about this. From the official Module Queue troubleshooting page: &lt;a href="https://docs.whmcs.com/9-0/troubleshooting/module-queue/" rel="noopener noreferrer"&gt;“WHMCS will not automatically retry a failed action. You must click Retry to attempt the failed action again.”&lt;/a&gt; The retry is a manual button click inside the admin UI. If no human opens that admin UI, the failed order sits in the queue. That is the silent-outage shape this post is about. The broader taxonomy of failures that look green on the dashboard lives in &lt;a href="https://velprove.com/blog/anatomy-of-a-silent-outage" rel="noopener noreferrer"&gt;the silent-outage taxonomy&lt;/a&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  The Module Queue is the silent-failure inspection point.
&lt;/h2&gt;

&lt;p&gt;The Module Queue is a WHMCS internal log of every automated module action that failed. From the docs: &lt;a href="https://docs.whmcs.com/9-0/troubleshooting/module-queue/" rel="noopener noreferrer"&gt;“The Module Queue list displays your WHMCS installation's failed automated actions. This includes any action that WHMCS performs using a module, either as part of the system cron tasks or in direct response to a user or admin action.”&lt;/a&gt; You can access it at &lt;code&gt;Utilities &amp;gt; Module Queue&lt;/code&gt; in the WHMCS admin area. The list shows the client name, the associated service or domain, the action that failed, the error details, and the time of the attempt. Two buttons sit next to each entry: Retry and Mark Resolved.&lt;/p&gt;

&lt;p&gt;WHMCS surfaces the queue count on the admin dashboard as a blue Pending Module Actions badge. From the WHMCS feature spotlight: &lt;a href="https://blog.whmcs.com/133510/feature-spotlight-module-queue" rel="noopener noreferrer"&gt;“These represent times that a WHMCS installation attempted to perform an action with an external system (via a module) but did not receive a successful response back.”&lt;/a&gt; The badge is helpful if you are already inside the admin area. It is not a monitor. It does not page you at 2 AM. It does not Slack your operations channel. It requires a human to open the dashboard and look at the badge.&lt;/p&gt;

&lt;p&gt;The corresponding API endpoint is &lt;a href="https://developers.whmcs.com/api-reference/getmodulequeue/" rel="noopener noreferrer"&gt;GetModuleQueue&lt;/a&gt; . It returns a JSON response with a &lt;code&gt;result&lt;/code&gt; field, a &lt;code&gt;count&lt;/code&gt; field, and a &lt;code&gt;queue&lt;/code&gt; array containing one object per failed action. Each queue entry carries the &lt;code&gt;serviceId&lt;/code&gt;, &lt;code&gt;moduleName&lt;/code&gt;, &lt;code&gt;moduleAction&lt;/code&gt;, &lt;code&gt;lastAttempt&lt;/code&gt; timestamp, and the verbatim &lt;code&gt;lastAttemptError&lt;/code&gt; message from the upstream provisioning module. That last field is the signal you actually want when you triage a red alert.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-monitor recipe: a single GetModuleQueue API monitor (free plan ready).
&lt;/h2&gt;

&lt;p&gt;The load-bearing recipe is one API monitor against &lt;code&gt;GetModuleQueue&lt;/code&gt;. It catches every queued failure regardless of which provisioning module produced it. It runs on the Velprove free plan. It takes about three minutes to configure.&lt;/p&gt;

&lt;p&gt;Set up a Velprove monitor of type &lt;strong&gt;API&lt;/strong&gt; (not multi-step, so your multi-step quota stays free for the optional 3-step chain in the next section). HTTP method &lt;code&gt;POST&lt;/code&gt;. URL &lt;code&gt;https://your.whmcs.example.com/includes/api.php&lt;/code&gt;. Set the request header &lt;code&gt;Content-Type&lt;/code&gt; to &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;. The form-encoded body:&lt;/p&gt;

&lt;p&gt;Add a single &lt;code&gt;json_path&lt;/code&gt; assertion: path &lt;code&gt;$.count&lt;/code&gt;, operator &lt;strong&gt;equals&lt;/strong&gt;, expected value &lt;code&gt;0&lt;/code&gt;. That is the entire recipe. When the queue is empty, the monitor stays green. When any module action fails (anywhere in your WHMCS install, across any provisioning module), the count goes above zero and the monitor flips red.&lt;/p&gt;

&lt;p&gt;On the Velprove free plan, set the interval to 5 minutes. On Starter ($19/month), set it to 1 minute. On Pro ($49/month), the floor drops to 30 seconds. The Velprove free plan also covers all six assertion types, so you have everything you need to express this monitor without upgrading.&lt;/p&gt;

&lt;p&gt;When the monitor flips red, the alert tells you the queue is no longer empty. To triage, hit &lt;code&gt;GetModuleQueue&lt;/code&gt; manually (curl, Postman, your browser) and read the &lt;code&gt;queue&lt;/code&gt; array. Each entry tells you which client, which service, which module, which action, and the exact error message the module returned. From there you have everything you need to walk the remediation flow that WHMCS documents at the &lt;a href="https://help.whmcs.com/m/provisioning/l/680014-resolving-a-failed-hosting-account-creation" rel="noopener noreferrer"&gt;Resolving a Failed Hosting Account Creation&lt;/a&gt; page. The 1-step recipe is the right starting point for any WHMCS-using operator. Most teams ship this one and never need the 3-step chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-step chain: trigger a test order and assert the queue stays clean.
&lt;/h2&gt;

&lt;p&gt;The more advanced recipe creates a real test order on every monitor run and then checks the queue. It catches a different class of failure: cases where the upstream module appears healthy to GetModuleQueue (because nothing real has been ordered recently) but actually fails when an order arrives. It is most useful when your order volume is low enough that the 1-step recipe could go days without exercising the provisioning path.&lt;/p&gt;

&lt;p&gt;The chain uses three API calls. The load-bearing detail is in the WHMCS AddOrder API documentation, which is explicit: &lt;a href="https://developers.whmcs.com/api-reference/addorder/" rel="noopener noreferrer"&gt;“For more flow control, this method ignores the ‘Automatically setup the product as soon as an order is placed.’ option. When you call this method, you must make a subsequent explicit call to AcceptOrder.”&lt;/a&gt; So AddOrder alone does not provision. You need AcceptOrder afterward to trigger the provisioning module.&lt;/p&gt;

&lt;p&gt;The chain (Velprove monitor type: multi-step). Every step uses the header &lt;code&gt;Content-Type: application/x-www-form-urlencoded&lt;/code&gt; and a form-encoded body:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: AddOrder.&lt;/strong&gt; &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;/includes/api.php&lt;/code&gt; (no real gateway fires because &lt;code&gt;paymentmethod=mailin&lt;/code&gt;). Body:&lt;/p&gt;

&lt;p&gt;Assert &lt;code&gt;json_path&lt;/code&gt; path &lt;code&gt;$.result&lt;/code&gt;, operator &lt;strong&gt;equals&lt;/strong&gt;, expected &lt;code&gt;success&lt;/code&gt;. In the step's &lt;em&gt;extract&lt;/em&gt; config, capture &lt;code&gt;$.orderid&lt;/code&gt; from the response into a variable named &lt;code&gt;order_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: AcceptOrder.&lt;/strong&gt; &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;/includes/api.php&lt;/code&gt;. Body:&lt;/p&gt;

&lt;p&gt;Assert &lt;code&gt;json_path&lt;/code&gt; path &lt;code&gt;$.result&lt;/code&gt;, operator &lt;strong&gt;equals&lt;/strong&gt;, expected &lt;code&gt;success&lt;/code&gt;. This call triggers the provisioning module against your cPanel, Plesk, or SolusVM box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: GetModuleQueue.&lt;/strong&gt; &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;/includes/api.php&lt;/code&gt;. Body:&lt;/p&gt;

&lt;p&gt;Assert &lt;code&gt;json_path&lt;/code&gt; path &lt;code&gt;$.count&lt;/code&gt;, operator &lt;strong&gt;equals&lt;/strong&gt;, expected &lt;code&gt;0&lt;/code&gt;. If your test order's provisioning failed, the failure entry is sitting at the top of the queue and the assertion trips.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;{{order_id}}&lt;/code&gt; template syntax in Step 2 is Velprove's flat variable interpolation: any name you used in a previous step's extract config can be referenced inside double braces in subsequent steps. Names are word-character only (no dots, no dashes). For the underlying variable-extraction semantics, see &lt;a href="https://velprove.com/blog/multi-step-api-monitoring-guide" rel="noopener noreferrer"&gt;the multi-step API monitoring guide&lt;/a&gt; .&lt;/p&gt;

&lt;p&gt;The chain fits Velprove's free plan exactly: 3 steps is the free cap. Starter raises it to 5, Pro to 10.&lt;/p&gt;

&lt;p&gt;One auto-setup footnote. The WHMCS &lt;a href="https://docs.whmcs.com/9-0/orders/order-statuses/" rel="noopener noreferrer"&gt;Order Statuses documentation&lt;/a&gt; notes: “If you have configured provisioning to occur while orders are in the Pending status, they will occur without you having accepted the order.” If your install runs in that mode, step 2 (AcceptOrder) is optional and a 2-step chain (AddOrder, then GetModuleQueue) is sufficient. Most resellers run in the safer default of provisioning on Accept, so the 3-step chain is the right shape for most readers.&lt;/p&gt;

&lt;p&gt;A cleanup note: every monitor run leaves a test order in &lt;code&gt;tblorders&lt;/code&gt;. Add a nightly cron that calls the WHMCS DeleteOrder API to garbage-collect every &lt;code&gt;velprove-canary&lt;/code&gt; tagged order from the previous day. Without cleanup, your orders table fills up with thousands of synthetic test records inside a year.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which recipe do you actually need?
&lt;/h2&gt;

&lt;p&gt;The 1-step recipe (a single &lt;code&gt;GetModuleQueue&lt;/code&gt; monitor) catches every queued failure that already happened in your install, across every module, regardless of who triggered it. It is cheap, read-only, and the recommended baseline for everyone including Free-plan readers. Most operators ship this and stop here.&lt;/p&gt;

&lt;p&gt;The 3-step chain catches a different class of failure: the case where AddOrder itself errors out (validation rejection, invalid payment method, malformed custom field), the case where AcceptOrder succeeds but the underlying module never gets called, and the case where the module call fails on a code path that the 1-step recipe would not have caught between organic orders. The two recipes are additive, not alternatives. Most teams add the chain only after the 1-step monitor has caught its first incident and they want active provisioning verification on top of the passive queue check.&lt;/p&gt;

&lt;p&gt;For the broader framework on which third-party-like systems (WHMCS-as-a-dependency is one) justify a synthetic monitor at all, see &lt;a href="https://velprove.com/blog/monitor-third-party-dependency-you-dont-own" rel="noopener noreferrer"&gt;the 3-of-12 rule for which dependencies to monitor synthetically&lt;/a&gt; . WHMCS scores high on every axis (blast radius across all customers, revenue attribution on the order path, no useful vendor status page because the vendor is you), so it lands in the must-monitor bucket for any WHMCS-running operator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The least-privilege service account.
&lt;/h2&gt;

&lt;p&gt;Do not point these monitors at your existing admin API credentials. Create a dedicated WHMCS admin role for the monitor service account. The role needs four API permissions and nothing else: &lt;code&gt;GetModuleQueue&lt;/code&gt;, &lt;code&gt;AddOrder&lt;/code&gt;, &lt;code&gt;AcceptOrder&lt;/code&gt;, and &lt;code&gt;GetClientsProducts&lt;/code&gt; (the last one if you want to extend the chain later to verify the service row landed correctly). Disable every other API capability on the role, including any read access to client billing data or server credentials.&lt;/p&gt;

&lt;p&gt;Create a dedicated API credential pair under that role and use it only for Velprove monitors. Rotate the credential quarterly with the same calendar reminder you use to rotate your other service credentials. If WHMCS supports IP allowlisting on the role (depends on your WHMCS version and any third-party security modules you have installed), restrict the credential to Velprove's monitor egress range so a leaked secret cannot be replayed from elsewhere.&lt;/p&gt;

&lt;p&gt;For the test client used by the 3-step chain: create a normal client account, tag it with a unique identifier like &lt;code&gt;velprove-canary&lt;/code&gt;, and use it as the sentinel &lt;code&gt;clientid&lt;/code&gt; for every AddOrder call. The tag makes the DeleteOrder cleanup cron trivial to write and keeps your production client tables clean. The hosting-stack adjacency story (what to monitor on the underlying cPanel/WHM box itself) lives in &lt;a href="https://velprove.com/blog/monitor-cpanel-whm-server" rel="noopener noreferrer"&gt;how to monitor the cPanel/WHM box itself&lt;/a&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  Alerting and incident response when the monitor flips red.
&lt;/h2&gt;

&lt;p&gt;Velprove's alert channels today: email on every plan, including Free. Slack, Discord, Microsoft Teams, and webhook on Starter ($19/month) and above. PagerDuty on Pro ($49/month). Route WHMCS monitor alerts to whichever channel your on-call human actually watches at 2 AM. For most resellers, that is PagerDuty for the on-call rotation plus a Slack mirror for shared visibility.&lt;/p&gt;

&lt;p&gt;Pick a home region from Velprove's 5 (North America, Europe, UK, Asia, Oceania) closest to your WHMCS install for the tightest baseline latency. All 5 regions are available on every plan, including Free. Each monitor runs from one region you pick, not fanned out across all five.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;GetModuleQueue&lt;/code&gt; reports &lt;code&gt;count &amp;gt; 0&lt;/code&gt;, your runbook is the WHMCS-documented remediation flow:&lt;/p&gt;

&lt;p&gt;Open the WHMCS admin area, navigate to &lt;code&gt;Utilities &amp;gt; Module Queue&lt;/code&gt;. Read the error code on each pending action. The error field carries the verbatim message from the upstream module. Click the client name or service name on each entry to jump into the affected client's profile and the Products/Services tab. Fix the underlying cause (username collision, server quota exceeded, API token expired, IP allowlist mismatch). Click Retry inside the Module Queue UI to re-attempt the failed action. The page displays the new attempt result immediately.&lt;/p&gt;

&lt;p&gt;The InMotion Hosting troubleshooting guide catalogues the common error shapes you will see in the queue: &lt;a href="https://www.inmotionhosting.com/support/edu/whm/how-to-troubleshoot-provisioning-issues-using-whmcs/" rel="noopener noreferrer"&gt;“Module Create Failed - Service ID: 4 - Error: Access denied”&lt;/a&gt; (cPanel rejected the credential), “Server Command Error - Curl Error - Couldn't connect to host (7)” (network partition or port 2087 blocked), “406 Not Acceptable” (ModSecurity rule fired), and “Allowed memory size of xxxxx bytes exhausted” (PHP memory_limit too low on the WHMCS host). Each has a known fix. The Velprove monitor surfaces the failure; the remediation stays in your hands. Velprove monitors are read-only observers and do not drive WHMCS workflow actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WHMCS is a high-value surface to monitor in the first place.
&lt;/h2&gt;

&lt;p&gt;The Lagom Client Theme cascade through 2024 (HostUS in February, Hosturly mid-year, DigiRDP later in the year) taught the hosting industry that WHMCS panels sit at the center of billing, identity, and provisioning, and that vulnerabilities in any one popular theme or add-on can cascade across the entire customer base. RSStudio shipped Lagom 2.2.7 in September 2024 with the security fix. WHMCS itself shipped a &lt;a href="https://blog.whmcs.com/security/" rel="noopener noreferrer"&gt;security update on June 3, 2025 covering v8.13, v8.12, and v8.11 LTS&lt;/a&gt; . Disclosure language was vague. The cadence is the point: the flows running through your WHMCS install warrant external monitoring you control, not just admin-dashboard widgets you have to remember to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patterns to avoid (honest about Velprove's primitive set).
&lt;/h2&gt;

&lt;p&gt;Five patterns WHMCS-community guides commonly recommend for provisioning monitoring do not fit Velprove's primitive set. Naming them is faster than pretending they are options.&lt;/p&gt;

&lt;p&gt;No polling primitive. Velprove's multi-step API monitor runs each step exactly once in sequence and records the result. “Wait 60 seconds after AddOrder, then keep hitting GetOrders until status flips to Active” is not expressible. The replacement is the monitor interval. If you need 30-second detection, set the interval to 30 seconds on the Pro plan.&lt;/p&gt;

&lt;p&gt;No time-relative freshness assertion. The six assertions are &lt;code&gt;status_code&lt;/code&gt;, &lt;code&gt;body_contains&lt;/code&gt;, &lt;code&gt;body_not_contains&lt;/code&gt;, &lt;code&gt;json_path&lt;/code&gt;, &lt;code&gt;response_time_ms&lt;/code&gt;, and &lt;code&gt;header_contains&lt;/code&gt;. “Assert this order was created within the last 5 minutes” is not a primitive. The replacement is your endpoint computing freshness server-side and returning 200 or 503, or a &lt;code&gt;json_path&lt;/code&gt; assertion against a static expected value like &lt;code&gt;$.count = 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No multi-page browser navigation through the WHMCS order wizard. The browser login monitor drives one form submit (the login page). It does not click through the order placement wizard, add items to the cart, fill the billing form, and complete checkout. Order creation in this post lives in the API path (AddOrder), not the browser path. The browser monitor pattern stays where it shines: client-area login coverage on &lt;code&gt;clientarea.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No retry-from-monitor. Velprove monitors are read-only. The Retry button inside the Module Queue UI is the operator action that re-attempts the failed module call. The monitor surfaces the failure; the operator runs the retry. Trying to make the monitor call AcceptOrder or the WHMCS RetryQueueItem API to self-heal a failed provision is an antipattern: it papers over the underlying cause (quota exceeded, credentials wrong, server full) and starts accumulating real provisioning errors at scale.&lt;/p&gt;

&lt;p&gt;No mobile push channel. Velprove's alert channels today are email, Slack, Discord, webhook, Microsoft Teams, and PagerDuty. There is no mobile push alert on any plan. Plan your alert routing around what exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the difference between monitoring my WHMCS portal login and monitoring the order and provisioning flow?
&lt;/h3&gt;

&lt;p&gt;A browser login monitor on your &lt;a href="https://velprove.com/blog/monitor-whmcs-portal" rel="noopener noreferrer"&gt;WHMCS client portal&lt;/a&gt; confirms that &lt;code&gt;clientarea.php&lt;/code&gt; renders and accepts customer credentials. It tells you the auth layer is up and the database row exists. It tells you nothing about whether the order, invoice, and provisioning chain that runs after login succeeds actually completes. The order-flow monitor in this post covers the AddOrder, AcceptOrder, and provisioning module call chain that produces a working hosting account. The two monitors catch different failure modes and are additive, not alternatives. Most operators run both: one browser login monitor for portal availability, one &lt;code&gt;GetModuleQueue&lt;/code&gt; API monitor for silent provisioning failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I monitor WHMCS provisioning failures without triggering real charges?
&lt;/h3&gt;

&lt;p&gt;Create a dedicated test client in WHMCS with a unique tag like &lt;code&gt;velprove-canary&lt;/code&gt; so your cleanup scripts can identify it. Set up a sentinel product SKU configured with &lt;code&gt;paymentmethod=mailin&lt;/code&gt; (bank transfer), which sits in pending without firing a real payment gateway. Run the 3-step chain (AddOrder, AcceptOrder, GetModuleQueue) against this test client and sentinel product. Schedule a nightly cron with the DeleteOrder API to garbage-collect &lt;code&gt;velprove-canary&lt;/code&gt; tagged orders so they do not accumulate in &lt;code&gt;tblorders&lt;/code&gt;. No customer-facing charges are ever generated, and your WHMCS database stays clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does the WHMCS Module Queue actually catch that a portal login monitor misses?
&lt;/h3&gt;

&lt;p&gt;Module-by-module provisioning failures that happen after the customer paid and logged in. cPanel &lt;code&gt;CreateAccount&lt;/code&gt; failing on quota exceeded. Plesk &lt;code&gt;CreateAccount&lt;/code&gt; failing on a subscription template mismatch. SolusVM &lt;code&gt;Create&lt;/code&gt; failing on stockout. Domain registrar module timeouts. ResellerClub authentication failures after API key rotation. WHMCS appends every one of these to the Module Queue with the verbatim error from the upstream module (Access denied, Couldn't connect to host (7), 406 Not Acceptable from ModSecurity, Allowed memory size exhausted). All of them are silent from the portal-login perspective: the &lt;code&gt;tblclients&lt;/code&gt; row exists, &lt;code&gt;clientarea.php&lt;/code&gt; works, the customer logs in to find an account that does not exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should I run the GetModuleQueue monitor?
&lt;/h3&gt;

&lt;p&gt;5-minute intervals on Velprove Free, 1-minute on Starter, 30-second on Pro. &lt;code&gt;GetModuleQueue&lt;/code&gt; is a cheap, read-only API call against your WHMCS install, so the frequency tradeoff is detection lag versus WHMCS server load, and WHMCS server load is not a real constraint at this endpoint. If your order volume is high enough that a 5-minute detection lag means several failed provisions before you find out, move to Starter at $19 per month for 1-minute intervals. If your order volume is low (under 50 new orders per day), 5-minute Free-plan intervals are fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I do this on the Velprove free plan with the 3-step multi-step monitor limit?
&lt;/h3&gt;

&lt;p&gt;Yes. The 3-step order chain (AddOrder, AcceptOrder, GetModuleQueue) fits the Free plan's 3-step multi-step monitor cap exactly. The simpler 1-step recipe (a single &lt;code&gt;GetModuleQueue&lt;/code&gt; API monitor) also fits Free and is the recommended starting point for most operators. For the underlying multi-step primitive, see &lt;a href="https://velprove.com/blog/multi-step-api-monitoring-guide" rel="noopener noreferrer"&gt;the multi-step API monitoring guide&lt;/a&gt; . Free includes 10 monitors total at 5-minute intervals, all six assertion types, and email alerts.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I monitor a WHMCS provisioning module if my install does not expose the API externally?
&lt;/h3&gt;

&lt;p&gt;If your WHMCS API is internal-only, the order-chain recipe in this post will not work as written. The cleanest fallback is to open the API to Velprove's published monitor egress IPs (see &lt;a href="https://velprove.com/ips" rel="noopener noreferrer"&gt;velprove.com/ips&lt;/a&gt; for the live list and JSON feed), with a dedicated low-privilege API service account that only has &lt;code&gt;GetModuleQueue&lt;/code&gt;, &lt;code&gt;AddOrder&lt;/code&gt;, &lt;code&gt;AcceptOrder&lt;/code&gt;, &lt;code&gt;GetClientsProducts&lt;/code&gt; permissions. If even that is off the table, fall back to a Velprove &lt;a href="https://velprove.com/blog/monitor-whmcs-portal" rel="noopener noreferrer"&gt;browser login monitor on the WHMCS client portal&lt;/a&gt; (free plan, one browser login monitor included at a 15-minute interval). The browser monitor catches login-layer failures but cannot catch the silent provisioning failures this post is about.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>uptime</category>
    </item>
    <item>
      <title>Monitor a Heroku App: Eco Sleep, Release Phase, Scheduler</title>
      <dc:creator>velprove</dc:creator>
      <pubDate>Tue, 26 May 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/velprove/monitor-a-heroku-app-eco-sleep-release-phase-scheduler-44nb</link>
      <guid>https://dev.to/velprove/monitor-a-heroku-app-eco-sleep-release-phase-scheduler-44nb</guid>
      <description>&lt;p&gt;&lt;strong&gt;The short version:&lt;/strong&gt; On June 10 2025 Heroku went down for up to 24 hours and Heroku's own status page went down with it, because both ran on the same affected infrastructure. External monitoring is not an extra. For 7 hours and 42 minutes it was the only signal anyone had. Beyond named incidents, Heroku has three platform primitives a 200 OK on your dyno URL cannot see: Eco web dynos sleep after 30 minutes of inbound idle, Release Phase failures email the deployer but leave your public URL serving yesterday's code, and Heroku Scheduler is documented as "expected but not guaranteed." Classic Cedar Eco ($5) and Basic ($7) dynos get zero native threshold alerting. You upgrade to a Standard-1x dyno at $25 per dyno per month just to unlock email alerts on response time. The Velprove free plan covers the same gap with 10 monitors total, 1 browser login monitor, and multi-step API monitors, no credit card, commercial use allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Heroku's own status page went down with the platform on June 10 2025
&lt;/h2&gt;

&lt;p&gt;On Tuesday June 10 2025, Heroku went down for up to 24 hours, and Heroku's own status page went down with it. The incident started at 06:00 UTC when an automated operating system update ran against production infrastructure that was meant to have automated upgrades disabled. The update restarted host networking, the routes did not reapply, and outbound connectivity for every dyno on every affected host severed at once. Heroku identified root cause at 13:42 UTC, seven hours and forty-two minutes after the first dynos failed. Customer impact persisted for up to 24 hours on the long tail.&lt;/p&gt;

&lt;p&gt;From &lt;a href="https://www.heroku.com/blog/summary-of-june-10-outage/" rel="noopener noreferrer"&gt;Heroku's own postmortem&lt;/a&gt; , published 2025-06-15:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"Our internal tools and the Heroku Status Page were running on this same affected infrastructure. This meant that as your applications failed, our ability to respond and communicate with you was also severely impaired." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is the entire reason this post exists. For the first eight hours of the incident, the vendor status page that every Heroku customer reflexively refreshes during an outage could not tell them anything, because the status page was inside the outage. External monitoring stopped being theoretical insurance and became the only signal anyone had. Heroku has since said no system changes will occur outside its controlled deployment process going forward. That is the right corrective action. It does not change the structural lesson: a status page sitting on the same platform as the product it reports on is a single point of failure, and external monitoring is the second point.&lt;/p&gt;

&lt;p&gt;The rest of this post is what an external monitor should watch on a Heroku app between outages of that scale, which is most of the time. Three platform primitives, one Standard-only alerting wedge, and four concrete Velprove monitors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a 200 OK on your Heroku app URL is not enough
&lt;/h2&gt;

&lt;p&gt;A single GET on your Heroku app URL watches one thing: the web dyno answering on port $PORT. That is the smallest part of most real Heroku deployments. Behind the web dyno sit Release Phase steps that run migrations and asset compiles before a new release promotes, Scheduler jobs that fire on cron and run as one-off dynos, worker dynos that drain queues with no inbound traffic at all, and an auto-restart cycle that bounces every dyno at least once every 24 hours. None of those have a public URL, and a status-code probe pointed at &lt;code&gt;/&lt;/code&gt; cannot see any of them.&lt;/p&gt;

&lt;p&gt;Free-tier Heroku ended on 2022-11-28. Eco dynos at $5 a month for a shared 1,000-hour pool replaced the old free tier, and most indie-hacker Heroku apps now run on Eco or Basic. The hosting-economics half of that decision is its own conversation, covered in &lt;a href="https://velprove.com/blog/uptime-monitoring-indie-hackers-side-projects" rel="noopener noreferrer"&gt;the indie-hacker free-stack guide&lt;/a&gt; . This post assumes you have already made that call and is about the platform surface a URL monitor cannot see.&lt;/p&gt;

&lt;p&gt;The reason the distinction matters: page-level failures change the page, so a URL monitor catches them. Platform-level failures degrade the product without changing the page. Your marketing site can keep serving a clean 200 for hours after the Scheduler job that bills your customers skipped a run, after Release Phase failed and left you on yesterday's code, or after the database the public URL queries silently lost its connection pool. The rest of this post is the platform layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eco dyno sleep is inbound-idle, the opposite of Railway
&lt;/h2&gt;

&lt;p&gt;Heroku Eco web dynos sleep when no web traffic arrives, not when the dyno stops sending outbound traffic. &lt;a href="https://devcenter.heroku.com/articles/eco-dyno-hours" rel="noopener noreferrer"&gt;Heroku's Eco Dyno Hours docs&lt;/a&gt; state the rule verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"If an app has an Eco web dyno and that dyno receives no web traffic in a 30-minute period, it sleeps. Eco web dynos do not consume Eco dyno hours while sleeping." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;And on wake:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"the dyno becomes active again after a short delay." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Heroku does not publish a cold-start latency number. Community observation puts it at a handful of seconds for Node, a few more for Rails or Django, longer for the JVM. The honest framing is a short delay, qualitatively, with the actual number determined by your stack.&lt;/p&gt;

&lt;p&gt;The mechanic runs in the opposite direction from Railway. On Railway, a service sleeps when it has not sent outbound traffic for 10 minutes, and an external probe arrives as inbound traffic that does not reset the sleep clock (covered in &lt;a href="https://velprove.com/blog/monitor-railway-app" rel="noopener noreferrer"&gt;the Railway platform-layer guide&lt;/a&gt; ). On Heroku Eco the clock is inbound. A Velprove HTTP probe on a 5-minute interval arrives as inbound web traffic six times every 30 minutes, so the Eco web dyno never sees a full 30 minutes of silence, so it never sleeps. The probe is keeping the dyno warm whether or not you want it to.&lt;/p&gt;

&lt;p&gt;That comes with a cost. The Eco dyno-hour pool is 1,000 hours shared across every Eco dyno on your account. A single Eco web dyno held awake 24/7 burns about 720 hours per month (24 hours times roughly 30 days). One always-awake Eco web dyno fits comfortably in the pool with headroom for a second small Eco service. Two always-awake Eco web dynos overflow into billed dyno time at the Basic per-second rate. The Velprove pattern on Eco is honest about that tradeoff: a 5-minute probe interval keeps your one production Eco web dyno warm and observable, and if you have a second Eco service you slow the second probe to a 10-minute interval or accept the overflow.&lt;/p&gt;

&lt;p&gt;For a Heroku-hosted SaaS where cold-start latency matters, an always-warm Eco web dyno is the correct configuration. For a side project that genuinely does not care about a few seconds of cold-start delay on first request, a slower probe interval saves pool hours at the cost of detection lag. The rule is the principle, not a number: match the probe interval to how fast the failure matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release Phase failures leave your public URL on yesterday's code
&lt;/h2&gt;

&lt;p&gt;Release Phase is the lifecycle stage that runs after a build and before a release is promoted to the dyno formation. It is where database migrations and asset compile steps typically live. When the release command fails, the new release does not promote. The public URL keeps serving the previous release.&lt;/p&gt;

&lt;p&gt;From &lt;a href="https://devcenter.heroku.com/articles/release-phase" rel="noopener noreferrer"&gt;Heroku's Release Phase docs&lt;/a&gt; :&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"If the &lt;code&gt;release&lt;/code&gt; command exits with a non-zero exit status, or if it's shut down by the dyno manager, the release fails. In this case, the release is not deployed to the app's dyno formation." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Heroku does send an email when this happens:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"An email notification is generated in the event of a release phase failure." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The honest wedge is sharper than "Release Phase fails silently." The email arrives. The problem is what the email covers and what it does not.&lt;/p&gt;

&lt;p&gt;The email goes to the deployer, the developer who pushed the release. On a one-developer indie project that is the same person who would have configured an external monitor. On a small team, the deployer is whichever developer last pushed, not the on-call engineer. On a larger team with a deploy bot or a CI/CD pipeline, the email may be going to a shared inbox no human reads. The notification is real, but it is point-to-point email to a known address, not a routable alert into PagerDuty or Slack.&lt;/p&gt;

&lt;p&gt;The bigger problem is what the URL looks like. From the outside, a Heroku app whose Release Phase just failed looks identical to a Heroku app where the release succeeded and did not break anything: the public URL returns 200, the HTML looks correct, and the responses are consistent. The previous release is still running. If the migration that just failed was the one that adds a column three new code paths depend on, the next time those code paths run they will 500, but right now the URL is fine. Nothing has actually deployed, and the URL still looks normal.&lt;/p&gt;

&lt;p&gt;The pattern that closes the gap is a build-version probe. Expose a &lt;code&gt;/version&lt;/code&gt; endpoint that returns the current git SHA, wired from an environment variable that Heroku sets at build time. Have a Velprove HTTP monitor assert &lt;code&gt;body_contains&lt;/code&gt; the SHA your CI just produced. When the release succeeds, the new SHA serves and the assertion passes. When Release Phase fails and the previous release stays live, the old SHA serves and the assertion fails on the next probe. The mechanic is exactly the same shape used on Render and Railway; the Heroku-specific framing is the lifecycle stage. The full assertion pattern is in the &lt;a href="https://velprove.com/blog/api-health-check-patterns" rel="noopener noreferrer"&gt;build-SHA assertion pattern&lt;/a&gt; guide; this section is the Release Phase framing on top of it.&lt;/p&gt;

&lt;p&gt;Recovery on Heroku is two commands. &lt;code&gt;heroku releases:retry&lt;/code&gt; reruns the release without a new build, useful when the failure was an external dependency such as a Postgres instance that was briefly unavailable. &lt;code&gt;heroku rollback&lt;/code&gt; promotes a prior release if the failed release uncovered something that needs a code fix. Either way, the monitor told you to run them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Heroku Scheduler is "expected but not guaranteed"
&lt;/h2&gt;

&lt;p&gt;Heroku Scheduler is a free add-on that runs jobs on a cron-like schedule by spawning a one-off dyno that executes the configured command. The killer detail is in &lt;a href="https://devcenter.heroku.com/articles/scheduler" rel="noopener noreferrer"&gt;Heroku's own Scheduler docs&lt;/a&gt; :&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"Scheduler job execution is expected but not guaranteed. Scheduler is known to occasionally (but rarely) miss the execution of scheduled jobs." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;And, in the same article:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;"In very rare instances, a job may be skipped. In very rare instances, a job may run twice." *&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read those sentences twice. Heroku is documenting that Scheduler can skip a run and can double-run a run, with no native alert for either case. If you bill customers from a Scheduler job, settle balances from a Scheduler job, or send a daily report from a Scheduler job, the platform has formally disclaimed the guarantee. The contract is best-effort.&lt;/p&gt;

&lt;p&gt;Both failure modes are silent from outside. A job that runs and exits non-zero produces logs in your platform-aggregated log stream, which you have to be looking at. A job that Scheduler skips produces nothing, because from Scheduler's side nothing happened. The Render counterpart (covered in &lt;a href="https://velprove.com/blog/monitor-render-hosted-app" rel="noopener noreferrer"&gt;the Render platform-layer guide&lt;/a&gt; ) at least emails on a failed run; Heroku Scheduler does not even emit that signal for the skip case.&lt;/p&gt;

&lt;p&gt;The pattern that works is a heartbeat URL the job hits at the end of its successful run, paired with a freshness endpoint a probe asserts against. The job writes a timestamp to Postgres or a key value store on durable success, after the work is done, not on entry. A small companion route reads that timestamp, computes its age, and returns 503 when the age exceeds the job cadence plus a grace window, 200 otherwise:&lt;/p&gt;

&lt;p&gt;A Velprove HTTP monitor asserts &lt;code&gt;status_code = 200&lt;/code&gt; on that endpoint. The endpoint flips to 503 the moment the job goes stale, so a 200 is the whole check. Both Scheduler failure modes, skipped run and run-that-exited-non-zero-without-writing-the-stamp, collapse into one signal: the timestamp did not advance. The detection lag is bounded by the probe interval, not by Scheduler.&lt;/p&gt;

&lt;p&gt;One discipline matters: the job must write the timestamp on real progress, not on entry. A job that fails partway through and exits non-zero before the final write looks correctly stale from the freshness endpoint. A job that writes the timestamp before doing the work would look fresh while never actually completing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eco and Basic dynos get no native threshold alerting
&lt;/h2&gt;

&lt;p&gt;This is the load-bearing economic wedge of the post. Heroku has a first-party alerting feature called Threshold Alerting that emails you or pages PagerDuty when response time or failed-response rate crosses a configured threshold. It runs on top of App Metrics, which is the dashboard view of your dyno's performance over time.&lt;/p&gt;

&lt;p&gt;Two quotes from &lt;a href="https://devcenter.heroku.com/articles/metrics" rel="noopener noreferrer"&gt;Heroku's Application Metrics docs&lt;/a&gt; define the tier boundary:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;"Application metrics aren't available for apps using &lt;code&gt;eco&lt;/code&gt; dynos." *&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"The Threshold Alerting feature is available to apps running on Professional dynos (&lt;code&gt;standard-1x&lt;/code&gt;, &lt;code&gt;standard-2x&lt;/code&gt; and &lt;code&gt;performance&lt;/code&gt;) and all Fir dynos." *&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The economic shape is this: classic Cedar Eco dynos at $5 a month and classic Cedar Basic dynos at $7 a month do not have App Metrics, so they cannot have Threshold Alerting, so they have zero native uptime alerting from Heroku at all. The cheapest classic dyno that includes Threshold Alerting is Standard-1x at $25 per dyno per month. That is a 5x jump in dyno cost for an Eco shop and a 3.5x jump for a Basic shop, paid not for more compute but for the right to receive an email when response time crosses a threshold.&lt;/p&gt;

&lt;p&gt;The Fir-generation entry-tier dyno (Heroku's next-generation Kubernetes-based runtime) does include alerting on its low-cost tier per the Threshold Alerting tier quote. The Eco-no-alerting claim scopes specifically to classic Cedar Eco dynos. If you are running on Fir already, your alerting story is different and worth checking against the current Fir docs.&lt;/p&gt;

&lt;p&gt;The third option is external. A Velprove free plan covers 10 HTTP monitors at a 5-minute interval, 1 browser login monitor at a 15-minute interval, multi-step API monitors up to 3 steps, and 1 status page, with email alerts on every plan including free. That gives a Cedar Eco shop response-time and failed-response alerting without changing dyno tier, plus the Release Phase and Scheduler coverage Threshold Alerting cannot give you even on a Standard dyno. The math is straightforward: $0 for external alerting versus $240 a year per dyno to unlock native alerting. The right answer is both, for most teams, but the cost of starting with external is zero and the marginal benefit is high.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the 4 Velprove monitors for a Heroku app
&lt;/h2&gt;

&lt;p&gt;Put the patterns above together and the Heroku-side coverage lands in four concrete monitors. All four fit inside the Velprove free plan: 10 monitors total at a 5-minute interval, 1 browser login monitor at a 15-minute interval, multi-step API monitors up to 3 steps, email alerts, SSL expiry monitoring, and 1 status page with a Velprove badge. Each monitor probes from one of 5 global regions you pick at setup time. Every plan picks from the same 5 regions; to cover multiple regions, you create multiple monitors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create a browser login monitor on the signed-in path.&lt;/strong&gt; This is the Velprove differentiator and the monitor that catches the most subtle Heroku failures, so it goes first in the canonical order. Create a new browser login monitor against your Heroku app's login URL with a dedicated low-privilege test user. The monitor drives a real browser, signs in as the test user, follows the post-login redirect, and asserts on the landing page. Under &lt;strong&gt;Customize detection&lt;/strong&gt;, switch &lt;strong&gt;Success verification&lt;/strong&gt; from the default URL-change to &lt;strong&gt;Page contains text&lt;/strong&gt; and set it to a string that only renders when a real database read succeeded: a customer name, an invoice ID, a known plan label. A Release Phase that fails the migration adding a column the login flow depends on will land the user on an error page with a 200 status, which a URL probe would miss and the browser login monitor catches. The monitor is free on every plan, including the free plan, at a 15-minute interval. &lt;strong&gt;Add an HTTP monitor on the public URL with a build-SHA assertion.&lt;/strong&gt; Create an HTTP monitor against your public Heroku app URL on a 5-minute interval. On the Verify step, add two Success Conditions in order: &lt;code&gt;status_code = 200&lt;/code&gt; and &lt;code&gt;body_contains&lt;/code&gt; set to the build SHA exposed at &lt;code&gt;/version&lt;/code&gt;. The body_contains rule turns the same probe into a Release Phase detector, because a stuck release keeps serving the previous SHA at a 200 URL. Pick the region closest to your real users. On Eco, this probe also keeps the dyno warm by arriving as inbound web traffic every 5 minutes, which resets the 30-minute sleep clock. &lt;strong&gt;Add an API monitor on the Scheduler heartbeat endpoint.&lt;/strong&gt; Create an HTTP monitor (API-shaped) against the freshness endpoint your Scheduler job updates on real success. Assert &lt;code&gt;status_code = 200&lt;/code&gt;. The endpoint returns 503 when the timestamp goes stale, so a 200 is the whole check. Match the probe interval to the job cadence: a daily Scheduler job is comfortable on a 5-minute probe with a 25-hour grace window in the endpoint logic; an hourly job wants a tighter grace window. The detection lag is bounded by your probe interval, not by Scheduler. &lt;strong&gt;Add a multi-step API monitor for deploy verification.&lt;/strong&gt; Create a multi-step API monitor. Step 1 hits &lt;code&gt;/version&lt;/code&gt; and captures &lt;code&gt;$.build_sha&lt;/code&gt; into a variable using a &lt;code&gt;json_path&lt;/code&gt; assertion. Step 2 hits a second route that compares its own runtime SHA against the captured value and returns non-2xx on mismatch. Multi-step monitors run each step once in sequential order, with the same 6 assertion types HTTP monitors use: &lt;code&gt;status_code&lt;/code&gt;, &lt;code&gt;body_contains&lt;/code&gt;, &lt;code&gt;body_not_contains&lt;/code&gt;, &lt;code&gt;json_path&lt;/code&gt;, &lt;code&gt;response_time_ms&lt;/code&gt;, and &lt;code&gt;header_contains&lt;/code&gt;. No polling, no retry-until, no wait-for-condition. The free plan covers multi-step up to 3 steps; Starter covers up to 5 and Pro up to 10. This monitor is the upgrade path from the body_contains assertion in monitor (2): the SHA comparison lives server-side in your app, so the setup survives every future deploy unchanged.&lt;/p&gt;

&lt;p&gt;That is four monitors out of your ten total slots: one browser login monitor, two HTTP monitors, and one multi-step monitor. The remaining six slots are room for a database health endpoint, a third-party API dependency, a second region on a critical path, or a second environment such as staging.&lt;/p&gt;

&lt;p&gt;Email alerts are included on every plan, including free. Slack, Discord, Microsoft Teams, and webhook alerts unlock on Starter. PagerDuty integration is on Pro for teams that route alerts into an on-call rotation. The free plan's status page carries a Velprove badge; the badge comes off on paid plans.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Velprove cannot catch on Heroku
&lt;/h2&gt;

&lt;p&gt;A monitor that pretends to catch everything is lying. The honest boundary on a Heroku app has four parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most multi-factor authentication flavors on the browser login monitor.&lt;/strong&gt; If your Heroku app's login flow requires an SMS code, an email code, a magic link, a push approval, or a passkey, the browser login monitor cannot complete it. Velprove cannot read your phone, your inbox, or your authenticator app. The monitor works on login flows where the dedicated test user can sign in with a username and password. For consumer SaaS where every user is forced through SMS or email-code MFA, the browser login monitor pattern is not the right tool; an HTTP monitor on a post-login API endpoint with a service token is. This is not a Heroku-specific limit, but it bites Heroku-hosted apps the same way it bites apps anywhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heroku platform internals that are invisible to an external probe.&lt;/strong&gt; Velprove sees what the public URL returns. Velprove does not see dyno-level CPU or memory pressure before the request reaches the dyno, the state of the router queue, or the internal health of Heroku Postgres beyond what your application code exposes. App Metrics on Standard-1x and up sees those; an external probe sees the consequences. The two views complement each other, and on a small Eco app the external view is the only view available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fir-generation entry-tier dyno alerting.&lt;/strong&gt; The Eco-no-alerting framing in this post scopes to classic Cedar Eco dynos. The Fir generation, Heroku's next-generation Kubernetes-based runtime, includes Threshold Alerting on its equivalent low-cost tier. If you are on Fir, your native alerting story is meaningfully different and worth checking against current Heroku docs before assuming this post's wedge applies to you. The external pattern still helps for the Scheduler skip and Release Phase build-SHA cases on Fir, because those are not threshold-shaped signals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dyno restart skew.&lt;/strong&gt; Per &lt;a href="https://devcenter.heroku.com/articles/dyno-restarts" rel="noopener noreferrer"&gt;Heroku's Dyno Restarts docs&lt;/a&gt; , the dyno manager restarts every dyno at least once per day on a jittered 24-hour-plus-216-random-minutes cycle. During a deploy, some dynos can be on the new release and some on the previous release for a short window. This is documented, intentional, and customer-tolerated behavior, not a failure mode you should wire alerting around. One sentence acknowledgment, not a wedge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;The Velprove free plan covers 10 monitors total at a 5-minute interval, 1 browser login monitor at a 15-minute interval, multi-step API monitors up to 3 steps, 5 global regions to choose from (one per monitor), email alerts, SSL expiry monitoring, and 1 status page with a Velprove badge. Commercial use is allowed on every plan, including free. No credit card required.&lt;/p&gt;

&lt;p&gt;That is enough to land the four-monitor Heroku set described above for a single production app: a browser login monitor on the signed-in path, an HTTP monitor on the public URL with a build-SHA assertion, an HTTP monitor on a Scheduler heartbeat endpoint, and a multi-step API monitor for deploy verification. &lt;a href="https://velprove.com/signup" rel="noopener noreferrer"&gt;Start with the free plan&lt;/a&gt;. The first monitor takes about three minutes to configure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I monitor a Heroku Eco dyno that sleeps after 30 minutes?
&lt;/h3&gt;

&lt;p&gt;Point a Velprove HTTP monitor at your public Heroku app URL on a 5-minute interval and assert &lt;code&gt;status_code = 200&lt;/code&gt; plus &lt;code&gt;body_contains&lt;/code&gt; on a static marker your real app emits. The probe arrives as inbound web traffic, which resets the 30-minute Eco sleep clock and wakes the dyno on the first hit after a sleep. The tradeoff is dyno-hour pool burn: a single Eco dyno held awake 24/7 consumes about 720 hours of the 1,000-hour Eco pool every month, which is fine for one app and tight if you run two. If you have a second Eco app on the same account, slow the probe interval or accept overflow billing.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I detect a Heroku Release Phase failure when the public URL still returns 200?
&lt;/h3&gt;

&lt;p&gt;Expose a &lt;code&gt;/version&lt;/code&gt; endpoint that returns the current git SHA from a build-time environment variable, then have a Velprove HTTP monitor assert &lt;code&gt;body_contains&lt;/code&gt; the SHA your CI just produced. When Release Phase fails, Heroku emails the deployer but does not promote the new release to the dyno formation, so your public URL keeps serving the previous build's SHA. The body_contains assertion fails on the next probe and Velprove pages you. The full multi-step capture-and-assert variant lives in our &lt;a href="https://velprove.com/blog/api-health-check-patterns" rel="noopener noreferrer"&gt;API health check patterns reference&lt;/a&gt; .&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Heroku Scheduler alert me when a job does not fire?
&lt;/h3&gt;

&lt;p&gt;No. Heroku's own Scheduler docs say job execution is expected but not guaranteed and that jobs may occasionally be skipped or run twice. Heroku sends no email and no webhook when a scheduled job fails to fire. The pattern that closes the gap is a heartbeat URL the job hits on real success, with a companion freshness endpoint that returns 503 when the timestamp goes stale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do I need external monitoring if I am paying for Heroku Standard dynos?
&lt;/h3&gt;

&lt;p&gt;Heroku's Threshold Alerting on Standard-1x and above watches response time and failed responses on the web dyno. It does not watch Scheduler runs, it does not catch a Release Phase failure that leaves you on the previous build SHA, and on June 10 2025 it could not tell you anything because Heroku's own status page went down with the platform on the same affected infrastructure. Threshold Alerting is a useful inside-the-platform signal. An external probe from outside Heroku is what gives you signal when Heroku itself is the failure. The two complement each other; one does not replace the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I monitor a Heroku app on the Velprove free plan?
&lt;/h3&gt;

&lt;p&gt;Yes. The Velprove free plan covers 10 monitors total at a 5-minute interval, one browser login monitor at a 15-minute interval, multi-step API monitors up to 3 steps, email alerts, SSL expiry monitoring, and 1 status page. Commercial use is allowed and no credit card is required. That is enough to land an HTTP probe on your web URL, a build-SHA assertion on &lt;code&gt;/version&lt;/code&gt;, a Scheduler heartbeat on a freshness endpoint, and a browser login monitor on the signed-in path of a Heroku-hosted SaaS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a Velprove probe keep my Heroku Eco dyno from sleeping?
&lt;/h3&gt;

&lt;p&gt;Yes, and that is the opposite of how Railway works. Heroku's Eco sleep clock is inbound-idle: if an Eco web dyno receives no web traffic in a 30-minute period, it sleeps. A Velprove HTTP probe arrives as inbound web traffic, so a 5-minute probe interval resets the sleep clock every 5 minutes and the dyno stays warm. The cost side of that decision is the 1,000-hour Eco pool shared across all Eco dynos on the account. One always-awake Eco web dyno burns about 720 hours per month, leaving room for one more small Eco service; two always-awake Eco dynos overflow into billed time. The Railway inverse, where service sleep is outbound-idle and an inbound probe does not reset the clock, is covered in &lt;a href="https://velprove.com/blog/monitor-railway-app" rel="noopener noreferrer"&gt;the Railway outbound-idle sleep writeup&lt;/a&gt; .&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>uptime</category>
    </item>
    <item>
      <title>The 3 of 12 Rule: Choosing Which Third-Party Dependencies to Monitor Synthetically</title>
      <dc:creator>velprove</dc:creator>
      <pubDate>Mon, 25 May 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/velprove/the-3-of-12-rule-choosing-which-third-party-dependencies-to-monitor-synthetically-3kl7</link>
      <guid>https://dev.to/velprove/the-3-of-12-rule-choosing-which-third-party-dependencies-to-monitor-synthetically-3kl7</guid>
      <description>&lt;p&gt;&lt;strong&gt;Triage:&lt;/strong&gt; Most SaaS apps run on 10 to 20 third-party dependencies. You cannot afford a custom monitor recipe for each one, and you should not try. The right move is dependency-graph triage. Rank every vendor on three axes: blast-radius (what breaks downstream when they fail), revenue-attribution (what dollars stop), and vendor-status-lag-history (how late their status badge tells the truth). The top 3 get a 5-minute synthetic of the exact call your app makes. The next 5 stay on the vendor's status page as a secondary signal. The bottom 4 you accept as quiet risk. This post walks the triage across the transactional email path, the vendor status page itself, and the webhook receiver you cannot host, using the Datadog External Provider Status launch from October 2025 as evidence that the largest SRE teams already treat the dependency-monitoring problem as tier-1.&lt;/p&gt;

&lt;h2&gt;
  
  
  You have 12 third-party dependencies. You should monitor 3 of them synthetically.
&lt;/h2&gt;

&lt;p&gt;Count the third-party dependencies your app calls in production. Auth provider. Email vendor. Payments. Object storage. CDN. DNS. Search. Queue. Webhook senders. AI provider if you have one. SMS provider if you have another. CRM API. Most SaaS apps land between 10 and 20 once you write the list down honestly. A custom synthetic per vendor on a 5-minute interval is ten to twenty monitors, and on most paid uptime tools that is real money per month before you have caught a single incident.&lt;/p&gt;

&lt;p&gt;The triage rule we run is this. Score every vendor on three axes from 0 to 3. Blast-radius is what breaks for your customers the moment the vendor goes dark. A payments vendor sits at 3. A vendor used for an offline weekly report sits at 0. Revenue-attribution is what dollars stop. A vendor in the checkout path is 3. A vendor in the marketing-email path is 1, because delayed marketing email rarely breaks the buy. Vendor-status-lag-history is how late their public status page has run on their last three incidents. A vendor that posted within 5 minutes of impact three times in a row scores 0. A vendor that has shown a 30-plus minute lag at least once scores 3.&lt;/p&gt;

&lt;p&gt;Add the three axes. Vendors that score 7 or higher belong in the top bucket. Cap the bucket at 3, the budget you actually have. Vendors that score 4 to 6 sit in the next 5: their status page is a secondary signal, you accept the lag, you do not run your own probe. Vendors that score 3 or under sit in the bottom 4: you accept that you find out late. The dependency-graph triage is the same triage every SRE team eventually arrives at. The work is making the scoring explicit and revisiting the list every quarter when vendors change.&lt;/p&gt;

&lt;p&gt;This is the generalized parent of two posts we already shipped. For the vendor-specific worked examples, see &lt;a href="https://velprove.com/blog/monitor-ai-app-when-llm-provider-degrades" rel="noopener noreferrer"&gt;the LLM-scoped version of this triage&lt;/a&gt; and &lt;a href="https://velprove.com/blog/monitor-stripe-api-health" rel="noopener noreferrer"&gt;the Stripe-scoped version of this triage&lt;/a&gt; . Both posts walk one vendor through the triage at full depth. This post walks the framework across a portfolio.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a synthetic of your own call actually buys you (the Datadog 2025 anchor)
&lt;/h2&gt;

&lt;p&gt;On October 21, 2025, Datadog launched External Provider Status alongside a free public companion at Updog.ai. The launch pitch is dependency-graph monitoring as a product category. In Datadog's own words: &lt;a href="https://www.datadoghq.com/blog/external-provider-status/" rel="noopener noreferrer"&gt;“Datadog External Provider Status provides real-time visibility into the health of more than 40 third-party providers, including 13 AWS services across global regions and widely used SaaS APIs such as GitHub, Stripe, and OpenAI.”&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The detection-time claim is the load-bearing number. From the same launch: &lt;a href="https://www.datadoghq.com/blog/external-provider-status/" rel="noopener noreferrer"&gt;“During a DynamoDB degradation on July 3, 2025, Datadog surfaced the issue 32 minutes before AWS acknowledged it on their status page.”&lt;/a&gt; Thirty-two minutes is the gap between when Datadog's customer telemetry started flagging the vendor and when the AWS status page caught up. The same 32-minute number appears in the companion Updog.ai launch post: &lt;a href="https://www.datadoghq.com/blog/updog-ai/" rel="noopener noreferrer"&gt;“Instead of depending on provider updates, Updog.ai is powered by aggregated, anonymized observability data and AI models.”&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Datadog's model is telemetry-derived: it needs APM agents installed and live customer traffic flowing to spot the vendor degradation in aggregate. Velprove's wedge is the opposite shape. A 5-minute synthetic of the exact dependency call your app makes, from one region you pick out of 5 regions available to choose from, with no agents and no instrumentation. It works on day one. If Datadog needed to ship a new product to close the 32-minute gap, you can ship the lightweight version yourself in 15 minutes per top-3 vendor.&lt;/p&gt;

&lt;p&gt;This is the structural reason a plain HTTP probe on the vendor's documented endpoint is not the same primitive. The vendor's endpoint often returns 200 while the call your app actually makes (the one with your auth, your headers, your payload shape) fails. The broader version of that argument lives in &lt;a href="https://velprove.com/blog/why-uptime-monitors-miss-outages" rel="noopener noreferrer"&gt;why HTTP probes alone miss vendor-degradation outages&lt;/a&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  What status-page-lag history tells you about a vendor
&lt;/h2&gt;

&lt;p&gt;The third triage axis (vendor-status-lag-history) is the one teams skip because it requires looking at the vendor's last three incidents and measuring the delta between impact start and the first Investigating update. The work is worth it, because the lag varies wildly across vendors. Stripe routinely posts within 5 to 10 minutes. GitHub posted the May 15 2026 Actions degradation 30 minutes after impact. AWS posted the July 3 2025 DynamoDB degradation 32 minutes after Datadog's telemetry caught it. The lag is structural to the vendor, not random.&lt;/p&gt;

&lt;p&gt;Third-party aggregator IsDown reports a wider gap across its provider pool. In their own product blog, they write: &lt;a href="https://isdown.app/blog/sendgrid-status-monitoring" rel="noopener noreferrer"&gt;“In January 2026, IsDown detected outages up to 2.2 hours before vendors acknowledged them, and caught 101 incidents that vendors never reported at all.”&lt;/a&gt; Two caveats on that number. It is self-reported product telemetry from a competing monitoring tool, not third-party-audited. And it is an aggregate across the IsDown provider pool in a single month, not a per-vendor claim. Read it as an upper bound on how bad vendor status pages can run, not as the median.&lt;/p&gt;

&lt;p&gt;The rule we use is rougher and easier to apply. Pull the vendor's status page incident history. Look at the last three incidents. If all three were posted within 5 minutes of impact, the status page is acceptable as a secondary signal. If any one of the three was posted 30 minutes or more after impact, the status page is not the monitor: a synthetic is. The broader structural argument that vendor status pages lag for the same reason customer-facing dashboards always lag the truth lives in &lt;a href="https://velprove.com/blog/verify-hosting-provider-uptime" rel="noopener noreferrer"&gt;vendor status-page lag is a structural problem&lt;/a&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  Triage worked example #1: the transactional email path
&lt;/h2&gt;

&lt;p&gt;Vendor candidates: SendGrid, Postmark, Mailgun. Score the axes. Blast-radius is HIGH: password resets, invoice receipts, double-opt-in confirmations, and magic-link auth all die together when the send API fails. Revenue attribution is MEDIUM: rarely the direct buy path, but onboarding-email failures kill activation and churn-recovery email failures cost real dollars at the long tail. Vendor-status-lag-history is LOW for the three named vendors: their status pages have been generally honest. Combined score: high enough to land in the top 3 for most SaaS shops.&lt;/p&gt;

&lt;p&gt;The Velprove recipe is a single API monitor. HTTP POST to the provider's send endpoint with a sentinel recipient address you own (something like &lt;code&gt;monitor@yourdomain.com&lt;/code&gt; routed to /dev/null on your end). Assert &lt;code&gt;status_code eq 202&lt;/code&gt; and &lt;code&gt;header_contains x-message-id&lt;/code&gt; on the response. Both assertions run on every Velprove plan, including Free. Each monitor run is one snapshot of the send path at that interval, not a poll: the monitor fires once, reads the response, and records the result. The synthetic catches API-side failures (rate limits, auth-key rotation breakage, provider 5xx). It does not catch deliverability failures (the message accepted by the vendor but never landing in the inbox). Velprove does not read inboxes and the browser login monitor cannot click an email link. For deliverability, layer a dedicated inbox-monitoring tool on top.&lt;/p&gt;

&lt;p&gt;A second pattern fits the same triage shape when the vendor exposes a customer dashboard you actually sign in to (Stripe Dashboard, AWS Console, SendGrid web app). Velprove's browser login monitor drives a real browser through the vendor's sign-in page with a dedicated low-privilege test account, then asserts on a post-login element only authenticated users see. If the vendor's auth backend is degraded but their public API returns 200, the API synthetic stays green and the browser login monitor flips red. Free plan includes one, running every 15 minutes from any of the 5 regions available to choose from. The two synthetics layer cleanly on the same vendor.&lt;/p&gt;

&lt;p&gt;The shape generalizes to any vendor whose API you call to produce a side-effect (send, charge, upload, dispatch). The foundational primitive is &lt;a href="https://velprove.com/blog/multi-step-api-monitoring-guide" rel="noopener noreferrer"&gt;the multi-step API monitor primitive&lt;/a&gt; , which the Free plan supports at up to 3 steps (5 on Starter, 10 on Pro). For email, 1 step covers it. The Stripe-checkout pattern needs 3 steps to land the full flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triage worked example #2: the vendor status page itself
&lt;/h2&gt;

&lt;p&gt;The triage question for the vendor status page is narrower than it sounds. The question is not “should I scrape my vendor's status page?” The question is “when can I trust this vendor's status page enough to skip running my own probe against the vendor?” The answer is the third axis. If the vendor's last three incidents posted within 5 minutes of impact, treating the status page as the secondary signal is reasonable. If any one of the three posted 30 minutes late, the status page is not the monitor.&lt;/p&gt;

&lt;p&gt;AWS sits in the second bucket. The July 3 2025 DynamoDB degradation is the Datadog anchor (32-minute gap) and the October 20 2025 AWS US-EAST-1 cascade is the worst-case example. Slack sits in the second bucket. Most SaaS APIs in the long tail (auth providers, search vendors, CRM webhooks) sit in the second bucket too: their status pages exist, but they update slowly because they are driven by manual SRE confirmation, not by customer telemetry. The right action is to scrape the status page as a secondary signal if you want richer context, but never to rely on it as the primary detection instrument for any vendor that scored 3 on the lag axis. The cluster-fold variant of this pattern (other failure modes that look green on the dashboard) lives in &lt;a href="https://velprove.com/blog/anatomy-of-a-silent-outage" rel="noopener noreferrer"&gt;the silent-outage taxonomy&lt;/a&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  Triage worked example #3: the webhook receiver you cannot host
&lt;/h2&gt;

&lt;p&gt;The triage question for webhook-driven vendors is constrained by what Velprove can and cannot do. Velprove does not host an inbound webhook receiver. We cannot accept the vendor's POST, parse it, and assert on the payload. That is a category of product (webhook capture and replay) we do not ship. The pattern that works inside Velprove's primitive set is two monitors that compose: trigger the workflow on the vendor side with one monitor, then check the downstream effect on your own application endpoint with a second monitor on the next interval.&lt;/p&gt;

&lt;p&gt;Stripe checkout is the canonical example. Monitor A is an API monitor that creates a test checkout session against Stripe's test mode. Monitor B is an API monitor that hits your own &lt;code&gt;/api/orders/test-canary&lt;/code&gt; endpoint (or whatever you name it), with a &lt;code&gt;json_path&lt;/code&gt; assertion that &lt;code&gt;$.status&lt;/code&gt; equals the literal string &lt;code&gt;paid&lt;/code&gt;. Your endpoint records the most recent test-canary state server-side, returns 200 with the static JSON if it has been updated by the Stripe webhook within your acceptable window, and returns 503 if it has not. Velprove does not need to know about the webhook itself. It only asserts on what your endpoint says about the webhook's effect. The deeper version of this pattern, including how to design the &lt;code&gt;/api/orders/test-canary&lt;/code&gt; endpoint, lives in &lt;a href="https://velprove.com/blog/monitor-stripe-webhooks" rel="noopener noreferrer"&gt;trigger-and-check-effect for webhooks&lt;/a&gt; . The shape applies to any vendor whose only outage signal is a webhook you cannot receive: SMS delivery callbacks, payment confirmations, CRM record-update events, build-finished notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-region pattern and partial regional degradation
&lt;/h2&gt;

&lt;p&gt;Velprove offers 5 regions available to choose from on every plan, including Free. Each monitor runs from one region you pick, not from all five at once. The triage implication is straightforward. For a vendor with global failure modes (Cloudflare data plane, AWS US-EAST-1 cascades), a single monitor in any one region catches the incident. For a vendor with regional failure modes (a CDN with PoP-specific issues, an auth provider whose European cluster degrades independently of US-East), you create one monitor per region you want to cover.&lt;/p&gt;

&lt;p&gt;The Cloudflare November 18 2025 outage is the clean global example. Cloudflare's own post-mortem at &lt;a href="https://blog.cloudflare.com/18-november-2025-outage/" rel="noopener noreferrer"&gt;blog.cloudflare.com/18-november-2025-outage&lt;/a&gt; records 11:20 UTC to 17:06 UTC, roughly 5 hours and 46 minutes of global data plane impact. Core CDN and security services returned HTTP 5xx status codes across every Cloudflare region. A Velprove synthetic from any of the 5 regions would have flipped red inside one monitor interval. No region selection wisdom was required.&lt;/p&gt;

&lt;p&gt;The October 20 2025 AWS DynamoDB cascade is the contrasting story. ThousandEyes' post-incident analysis at &lt;a href="https://www.thousandeyes.com/blog/aws-outage-analysis-october-20-2025" rel="noopener noreferrer"&gt;thousandeyes.com/blog/aws-outage-analysis-october-20-2025&lt;/a&gt; documents the shape: a DynamoDB DNS race condition surfaced at 6:49 AM UTC October 20, AWS engineers identified the cause by 7:26 AM UTC, DNS was fully restored between 9:25 and 9:40 UTC, and EC2 instance launches continued failing until 8:50 PM UTC, with Redshift cluster backlogs not cleared until 11:05 AM UTC October 21. The customer-visible window ran over 15 hours. Many of the downstream phases hit US-EAST-1 specifically. A monitor from a non-US region would have stayed green for the EC2-launch phase while a US-region monitor turned red. That asymmetry is the case for putting your top-3 vendor monitors in two or three regions when you can spend the monitor budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to monitor a dependency synthetically (the third bucket)
&lt;/h2&gt;

&lt;p&gt;The honest counterweight to the triage rule is the bottom-4 bucket. Some vendors do not justify a synthetic, because the cost of running the monitor (the time to set it up, the alert noise, the slot it takes in your monitor budget) exceeds the cost of finding out late.&lt;/p&gt;

&lt;p&gt;Three concrete shapes land in the bottom bucket reliably. A vendor used for an offline batch report that runs nightly: a 10-minute outage at 4 AM costs nothing real, and your nightly job retries on its own. A vendor used for a low-traffic internal admin feature: you will find out the next time you click the button, which is rare enough that the monitor is overhead. A vendor with a fast, honest status page and an email-subscription pipeline where 5-minute-late detection is acceptable to your operation. Calling these out explicitly is part of the triage: the rule is “3 of 12,” not “all 12.” The point of triage is to spend your monitor budget where it earns its keep, and to consciously accept the risk on the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patterns to avoid (honest about Velprove's primitive set)
&lt;/h2&gt;

&lt;p&gt;Five patterns commonly recommended for third-party API monitoring do not fit Velprove's primitive set. Naming them is faster than pretending they are options.&lt;/p&gt;

&lt;p&gt;No polling primitive. Velprove's multi-step API monitor runs each step exactly once in sequence, then records the result. There is no “keep hitting this endpoint until X” option, no retry-until-success loop, no condition-wait. The replacement is the monitor interval itself. If you need 30-second granularity, set the interval to 30 seconds on the Pro plan.&lt;/p&gt;

&lt;p&gt;No time-relative assertion type. The six assertions Velprove supports are &lt;code&gt;status_code&lt;/code&gt;, &lt;code&gt;body_contains&lt;/code&gt;, &lt;code&gt;body_not_contains&lt;/code&gt;, &lt;code&gt;json_path&lt;/code&gt;, &lt;code&gt;response_time_ms&lt;/code&gt;, and &lt;code&gt;header_contains&lt;/code&gt;. There is no “assert this timestamp field is within the last 60 seconds” primitive. The replacement is your endpoint computing freshness server-side and returning 200 or 503, or a &lt;code&gt;json_path&lt;/code&gt; assertion against a static expected value.&lt;/p&gt;

&lt;p&gt;No percentile latency thresholds. &lt;code&gt;response_time_ms&lt;/code&gt; is a per-request budget, not a p95 or p99 aggregate. The replacement is to set a per-request threshold that allows for some single-request noise, and to configure your alert rule to fire on N consecutive failures. The same goal (catch sustained slowdown, not single slow requests) is met by the consecutive-failure rule.&lt;/p&gt;

&lt;p&gt;No inbound webhook receiver. As described in worked example 3, Velprove does not host an endpoint that catches third-party webhooks. The replacement is trigger-and-check-effect: two monitors that compose, where the second asserts on your own application state after the vendor's webhook has had time to fire.&lt;/p&gt;

&lt;p&gt;No distributed tracing, no RUM. Velprove is the outside-in synthetic layer. APM tracing (Datadog, Honeycomb) and Real User Monitoring (Splunk, LogicMonitor) are complementary categories, not replacements. The right view of the dependency call from inside your application is the trace; the right view from outside is the synthetic.&lt;/p&gt;

&lt;p&gt;One final note on alert channels. Today, Velprove's alert channels are email (every plan), Slack, Discord, webhook, and Microsoft Teams (Starter and above), and PagerDuty (Pro). There is no mobile push channel on any plan today. Plan your alert routing around what exists, not what should exist. The opposite-prescription view of dependency monitoring (why your own &lt;code&gt;/healthz&lt;/code&gt; should NOT deep-probe these dependencies inside a liveness probe) lives in &lt;a href="https://velprove.com/blog/api-health-check-patterns" rel="noopener noreferrer"&gt;the inverse view, why your own /healthz should NOT deep-probe these dependencies&lt;/a&gt; . The complement holds: synthetic-from-outside, plain-liveness-from-inside.&lt;/p&gt;

&lt;p&gt;ThousandEyes' analysis of the October 20 2025 AWS incident captures the recovery-shape implication well: &lt;a href="https://www.thousandeyes.com/blog/aws-outage-analysis-october-20-2025" rel="noopener noreferrer"&gt;“Recovery timelines are sums of dependent phases, not parallel operations.”&lt;/a&gt; The triage rule above tells you which vendors to monitor. The recovery shape tells you why your incident playbook should keep the monitor running through the all-clear: the vendor's status page going green is the first phase, not the last.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I decide which of my third-party dependencies to monitor synthetically?
&lt;/h3&gt;

&lt;p&gt;Score every vendor on three axes from 0 to 3. Blast-radius is what breaks for your customers when the vendor goes dark (payments 3, weekly report 0). Revenue-attribution is what dollars stop (checkout 3, marketing email 1). Vendor-status-lag-history is how late the vendor posted its last three incidents (within 5 minutes 0, 30+ minutes 3). Sum the axes. Vendors scoring 7 or higher belong in the top 3. Vendors scoring 4 to 6 belong in the next 5, with the vendor status page as secondary signal. Vendors scoring 3 or under sit in the bottom 4: you consciously accept that you find out late. Revisit the list every quarter when vendors and traffic patterns change.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the realistic monthly cost of running a synthetic monitor per third-party vendor?
&lt;/h3&gt;

&lt;p&gt;On Velprove's Free plan, three synthetic monitors on your top-3 vendors costs $0 per month, assuming your total monitor count stays under the 10-monitor Free cap. Free includes 5-minute intervals, multi-step API monitors up to 3 steps, 1 browser login monitor (every 15 minutes), all six assertion types, and email alerts, with commercial use allowed. Starter at $19 per month unlocks 1-minute intervals plus Slack, Discord, webhook, and Teams channels. PagerDuty ships on Pro at $49 per month. By comparison, Datadog Synthetic prices per-test-per-region: three vendor synthetics from three regions runs into low-three-figures monthly at Datadog's current list price.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know when a vendor's status page is reliable enough that I do not need my own monitor?
&lt;/h3&gt;

&lt;p&gt;Pull the vendor's status page incident history and look at the last three incidents. Measure the gap between impact start (usually disclosed in the Resolved update) and the first Investigating update. If all three incidents posted within 5 minutes of impact, the status page is acceptable as a secondary signal: you can lean on it instead of running your own probe. If any one of the three incidents posted 30 minutes or more after impact, the status page is not the monitor. Stripe sits in the first bucket. AWS, Slack, and most long-tail SaaS sit in the second. Status page subscriptions are still useful for downstream context, even for vendors in the second bucket. They just are not the primary detection instrument.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I monitor a vendor whose API is bursty and noisy on the happy path?
&lt;/h3&gt;

&lt;p&gt;Bursty vendors generate single-request slow responses that are not real incidents. The per-request &lt;code&gt;response_time_ms&lt;/code&gt; assertion is per-request, not an aggregate, so a single slow response will trip a raw threshold. The fix is two configuration choices. First, set &lt;code&gt;response_time_ms&lt;/code&gt; to a threshold that allows for some single-request noise (often 2x or 3x the observed p50 from your own client telemetry). Second, configure the monitor's alert rule to fire on N consecutive failures instead of a single failure. Three consecutive 5-minute checks failing is 10 to 15 minutes of sustained degradation, which is the signal you actually want. The consecutive-failure rule is available on every plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I monitor a vendor whose only outage signal is a webhook I cannot receive in Velprove?
&lt;/h3&gt;

&lt;p&gt;Velprove does not host an inbound webhook receiver. We cannot accept the vendor's POST and parse the payload. The pattern that works is trigger-and-check-effect with two composed monitors. Monitor A is an API monitor that triggers the workflow on the vendor side (POST a test checkout, dispatch a test SMS, kick off a test build). Monitor B is an API monitor that hits your own application endpoint (&lt;code&gt;/api/canary/whatever&lt;/code&gt;) on the next monitor interval, with a &lt;code&gt;json_path&lt;/code&gt; assertion against a static expected value. Your endpoint records the most recent webhook-driven state server-side and returns 200 with the static JSON when the webhook arrived, or 503 when it did not. The deeper version with the Stripe checkout shape lives in &lt;a href="https://velprove.com/blog/monitor-stripe-webhooks" rel="noopener noreferrer"&gt;monitor Stripe webhooks&lt;/a&gt; .&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I do this on the free plan?
&lt;/h3&gt;

&lt;p&gt;Yes. Velprove's Free plan includes 10 monitors at a 5-minute interval, multi-step API monitors up to 3 steps, 1 browser login monitor (every 15 minutes), HTTP and API monitors with all six assertion types, and email alerts. Three synthetic API monitors on your top-3 vendors fit inside Free as long as your overall monitor count stays under 10. No credit card. Commercial use allowed.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>uptime</category>
    </item>
    <item>
      <title>The GitHub Actions May 2026 Degradation: A Detection-Time Teardown</title>
      <dc:creator>velprove</dc:creator>
      <pubDate>Sat, 23 May 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/velprove/the-github-actions-may-2026-degradation-a-detection-time-teardown-5d09</link>
      <guid>https://dev.to/velprove/the-github-actions-may-2026-degradation-a-detection-time-teardown-5d09</guid>
      <description>&lt;p&gt;Liquid syntax error: Variable '{{% raw %}' was not properly terminated with regexp: /\}\}/&lt;/p&gt;
</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>uptime</category>
    </item>
    <item>
      <title>Monitor a Railway App: Sleep, Private Net, Cron Services</title>
      <dc:creator>velprove</dc:creator>
      <pubDate>Wed, 20 May 2026 14:00:02 +0000</pubDate>
      <link>https://dev.to/velprove/monitor-a-railway-app-sleep-private-net-cron-services-3l75</link>
      <guid>https://dev.to/velprove/monitor-a-railway-app-sleep-private-net-cron-services-3l75</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; To monitor a Railway app properly, you have to probe it from outside Railway. Railway's own healthcheck only runs at deploy time and explicitly is not for continuous monitoring. Its sleep timer is outbound-driven so an external probe does not keep a service awake, services on &lt;code&gt;*.railway.internal&lt;/code&gt; are unreachable from the public internet, and a cron service has no native did-not-fire alert. Four Velprove patterns close those gaps: a public HTTP monitor on the web service, a &lt;code&gt;/deps&lt;/code&gt; probe for private services, a heartbeat probe for crons, and a browser login monitor on the real signed-in path. Free-tier Railway spin-down economics are a separate hosting decision covered in &lt;a href="https://velprove.com/blog/uptime-monitoring-indie-hackers-side-projects" rel="noopener noreferrer"&gt;the indie-hacker free-stack guide&lt;/a&gt;. Every monitor probes from one of 5 global regions you pick, on the Velprove free plan. No credit card required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Railway's native healthcheck is not your uptime monitor
&lt;/h2&gt;

&lt;p&gt;Railway has a built-in healthcheck and it is not what most people assume. &lt;a href="https://docs.railway.com/reference/healthchecks" rel="noopener noreferrer"&gt;Railway's healthchecks reference&lt;/a&gt; reads: &lt;em&gt;"The healthcheck endpoint is currently not used for continuous monitoring as it is only called at the start of the deployment, to ensure it is healthy prior to routing traffic to it."&lt;/em&gt; That single sentence is the entire reason this post exists. Railway itself is telling you the native healthcheck is a deploy-time gate, not a runtime alert.&lt;/p&gt;

&lt;p&gt;The mechanic is precise. When a new deployment is triggered, Railway repeatedly queries the configured healthcheck endpoint until it receives an HTTP 200, then activates the deployment and starts routing traffic. The default timeout is 300 seconds. The probe originates from &lt;code&gt;healthcheck.railway.app&lt;/code&gt;, which matters only if you have host-restricted access on that route. Once the new instance is live, Railway stops calling the endpoint. It does not come back later to confirm the instance is still answering.&lt;/p&gt;

&lt;p&gt;Two consequences follow. First, an instance that passes its deploy-time check and then stops responding an hour later produces zero signal from the native healthcheck, because the native healthcheck is not watching anymore. Second, an endpoint that returns 200 with an empty body or a stale cached error page will satisfy the gate just as easily as a real, working endpoint will. Deploy-time gating and runtime monitoring are two different problems, and Railway covers the first one. The second one is an external probe's job. This post is about the platform surface a 200 OK on your public URL cannot see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service sleep is outbound-driven, not inbound-idle
&lt;/h2&gt;

&lt;p&gt;This is the Railway fact drafters get wrong most often, so it goes early. &lt;a href="https://docs.railway.com/reference/app-sleeping" rel="noopener noreferrer"&gt;Railway's app-sleeping docs&lt;/a&gt; state the rule verbatim: &lt;em&gt;"For Railway to put a service to sleep, a service must not send outbound traffic for at least 10 minutes."&lt;/em&gt; And, also verbatim: &lt;em&gt;"Inbound traffic is excluded from considering when to sleep a service."&lt;/em&gt; The clock that triggers sleep is outbound. The clock has nothing to do with how many requests arrive at your service.&lt;/p&gt;

&lt;p&gt;What counts as outbound is broader than most people expect. Telemetry pushed to a logging or APM service, database connection pool keepalives, NTP queries, requests to another service in the same project over the private network, and external API calls all count as outbound traffic that keeps the service awake. What does not count is anything arriving at your service, including a request from a customer's browser and a probe from an external monitor.&lt;/p&gt;

&lt;p&gt;That last point is the one to internalize: &lt;strong&gt;a Velprove HTTP probe is inbound traffic from Railway's perspective, so it does not keep your Railway service awake.&lt;/strong&gt; A probe will wake a slept service on the first hit, because Railway wakes a slept service on any inbound request from the internet or from another service in the same project over the private network. But it will not prevent the next sleep. Ten minutes after your service's last outbound packet, it sleeps again, regardless of how often the probe arrives. If you need the service awake, do that with outbound activity originating inside the service: a periodic outbound heartbeat to a logging or telemetry endpoint is the honest way. Treat "monitoring keeps my service warm" as a trap on Railway, because the mechanic runs in the opposite direction from inbound-idle platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Private services on &lt;code&gt;*.railway.internal&lt;/code&gt; are invisible from outside
&lt;/h2&gt;

&lt;p&gt;Railway exposes a private network between services in the same project. &lt;a href="https://docs.railway.com/networking/private-networking" rel="noopener noreferrer"&gt;Railway's private-networking docs&lt;/a&gt; define the hostname pattern: &lt;em&gt;"&lt;code&gt;&amp;lt;service-name&amp;gt;.railway.internal&lt;/code&gt;. For example, a service named api would be reachable at &lt;code&gt;api.railway.internal&lt;/code&gt;."&lt;/em&gt; The transport is an encrypted Wireguard mesh, which is why the docs consider HTTP over the mesh acceptable: the tunnel itself is encrypted. Isolation is per-project and per-environment, so services in a different project or a different environment cannot resolve your &lt;code&gt;railway.internal&lt;/code&gt; hostnames at all.&lt;/p&gt;

&lt;p&gt;The load-bearing consequence for monitoring is structural. &lt;code&gt;*.railway.internal&lt;/code&gt; is unreachable from the public internet by design. An external probe, including a Velprove monitor running from any of the 5 global regions, cannot resolve those names and cannot reach those ports. There is no flag to flip and no header to add. The private network is private. That is the whole feature.&lt;/p&gt;

&lt;p&gt;The pattern that works is a public companion route. Pick one of the public web services in the project and add a route, conventionally &lt;code&gt;/deps&lt;/code&gt;, that exercises the actual dependency call you care about and returns 200 only if the private service responded correctly. A Velprove HTTP monitor against &lt;code&gt;/deps&lt;/code&gt; then gives you an external signal for a structurally internal service. Frame this honestly to yourself: &lt;code&gt;/deps&lt;/code&gt; is a userland convention, not a Railway primitive. The probe is watching the public companion, and the companion is watching the private dependency. If the companion lies or stops being deployed, the probe lies too. Keep the route's implementation small and obvious, and put the same Wireguard-side call your real code uses behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cron-as-service has no didn't-fire alert
&lt;/h2&gt;

&lt;p&gt;Railway crons are not a separate service type. They are a setting on a normal service. &lt;a href="https://docs.railway.com/reference/cron-jobs" rel="noopener noreferrer"&gt;Railway's cron-jobs reference&lt;/a&gt; describes the model: you define a 5-field crontab string, in UTC, on a service's settings. On schedule, Railway invokes the service's start command. The service is expected to do its work and terminate. The minimum frequency is every 5 minutes. On Render, a cron is its own separate billable service; on Railway it is a setting on a normal service, and that difference shapes everything downstream (see &lt;a href="https://velprove.com/blog/monitor-render-hosted-app" rel="noopener noreferrer"&gt;the Render platform-layer guide&lt;/a&gt; for the contrast).&lt;/p&gt;

&lt;p&gt;The concurrency rule is the one that bites silently. Railway's docs state, verbatim: &lt;em&gt;"If a previous execution is still running when the next scheduled execution is due, Railway will skip the new cron job."&lt;/em&gt; That is the silent-skip failure mode. A cron that hangs once because a downstream API is slow can quietly suppress every subsequent run until you notice the data is stale. From outside Railway, a hung cron and a skipped cron look identical: nothing happened. No surfaced email, no surfaced webhook for "the next run did not start."&lt;/p&gt;

&lt;p&gt;The pattern that works is a heartbeat. The cron writes a timestamp on real success into Postgres or a key value store, after the work is durably done, not on entry. A small companion web service reads that timestamp, computes its age, and returns 503 when the cron has been quiet longer than its expected cadence plus a grace window, 200 otherwise. A Velprove HTTP monitor against &lt;code&gt;/last-run/&amp;lt;job&amp;gt;&lt;/code&gt; asserts &lt;code&gt;status_code = 200&lt;/code&gt;. The endpoint flips to 503 the moment the cron goes stale, and the monitor catches that within one probe interval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Velprove does not receive passive heartbeats from your cron.&lt;/strong&gt; The freshness logic lives on your &lt;code&gt;/last-run/&amp;lt;job&amp;gt;&lt;/code&gt; endpoint, on your service, and Velprove asserts the status code from outside. A static &lt;code&gt;body_contains&lt;/code&gt; assertion that looks for today's date does not work for this; the monitor stores whatever string you type once at setup, then keeps asserting that stale value forever. Let the endpoint compute freshness server-side, and let the status code carry the signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify your deploy actually came up: the &lt;code&gt;/version&lt;/code&gt; SHA pattern
&lt;/h2&gt;

&lt;p&gt;Railway auto-deploys on every push to the connected branch by default. A green deploy in the Railway dashboard means the native healthcheck returned 200 once at activation. It does not mean the build that came up is the build you intended, and it does not mean the build is still serving correct responses now.&lt;/p&gt;

&lt;p&gt;The cheap fix is a &lt;code&gt;/version&lt;/code&gt; endpoint that returns the current git SHA, wired from an environment variable Railway sets at build time. Two ways to assert it with Velprove, and the right one depends on where you want the SHA comparison to live.&lt;/p&gt;

&lt;p&gt;The recommended form is a multi-step API monitor: Step 1 hits &lt;code&gt;/version&lt;/code&gt; and captures &lt;code&gt;$.build_sha&lt;/code&gt; into a variable, Step 2 calls a second route that compares its own runtime SHA against the captured value and returns non-2xx on mismatch. The comparison lives server-side in your app, the monitor just orchestrates, and the setup survives every future deploy unchanged. Available on every plan including free up to 3 steps.&lt;/p&gt;

&lt;p&gt;The shorter setup is a plain HTTP monitor with &lt;code&gt;body_contains&lt;/code&gt; set to the SHA your build just produced. It works for the current deploy and stales on the next, because the deployed app starts returning the new SHA while the monitor keeps asserting the old one. Use this form only when your CI/CD pipeline updates the assertion on every deploy via Velprove's &lt;code&gt;PUT /api/checks/&amp;lt;id&amp;gt;&lt;/code&gt; API. When a deploy reports green but serves a stale or wrong build, either assertion fails and the monitor pages you. Velprove does not provide a native deploy-skew detector; your &lt;code&gt;/version&lt;/code&gt; assertion is the detector.&lt;/p&gt;

&lt;p&gt;The full multi-step capture-and-assert flow, including the &lt;code&gt;X-Expected-SHA&lt;/code&gt; header variant for capturing a value from one step and asserting it in the next, is already walked through in &lt;a href="https://velprove.com/blog/api-health-check-patterns" rel="noopener noreferrer"&gt;the multi-step build_sha pattern in the API health-check guide&lt;/a&gt;. If multi-step is new, the same flow framed for API teams is in &lt;a href="https://velprove.com/blog/multi-step-api-monitoring-guide" rel="noopener noreferrer"&gt;the multi-step API monitoring walkthrough&lt;/a&gt;. This section is the Railway-specific framing on top of that pattern, not a re-derivation of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four Railway monitors in Velprove (free plan)
&lt;/h2&gt;

&lt;p&gt;Put the patterns above together and the Railway-side coverage lands in four concrete monitors. All four fit inside the Velprove free plan: 10 monitors total, a 5-minute HTTP interval, one browser login monitor at a 15-minute interval, multi-step API monitors up to 3 steps, email alerts, and 1 status page. Each monitor probes from one of 5 global regions you pick at setup time. If your Railway service runs Next.js, pair this set with &lt;a href="https://velprove.com/blog/monitor-nextjs-app-production" rel="noopener noreferrer"&gt;how to monitor a Next.js app in production&lt;/a&gt; for the render-layer half.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(a) Public web-service HTTP probe.&lt;/strong&gt; A plain HTTP monitor against your public Railway URL, or its public custom domain, asserting &lt;code&gt;status_code = 200&lt;/code&gt; and a &lt;code&gt;body_contains&lt;/code&gt; rule on a static string that only your real app emits (a footer tagline, a known marker in the HTML). The &lt;code&gt;body_contains&lt;/code&gt; rule keeps a cached gateway error page that happens to return 200 from passing. Set the interval to 5 minutes on free or 1 minute on a paid plan, and pick whichever of the 5 global regions is closest to your real customers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(b) Private-service &lt;code&gt;/deps&lt;/code&gt; probe.&lt;/strong&gt; You cannot point a Velprove monitor at &lt;code&gt;db.railway.internal&lt;/code&gt; or &lt;code&gt;worker.railway.internal&lt;/code&gt;, because those names resolve only inside your project's Wireguard mesh. Expose a &lt;code&gt;/deps&lt;/code&gt; route on a public service in the same project that calls the private dependency and returns 200 only on real success. Point a Velprove HTTP monitor at &lt;code&gt;/deps&lt;/code&gt;, assert &lt;code&gt;status_code = 200&lt;/code&gt;, and the private service becomes externally observable without giving it a public surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(c) Cron heartbeat probe.&lt;/strong&gt; On the companion route that reports cron freshness, set up an HTTP monitor against &lt;code&gt;/last-run/&amp;lt;job&amp;gt;&lt;/code&gt; asserting &lt;code&gt;status_code = 200&lt;/code&gt;. The endpoint returns 503 when the cron has gone stale, so a 200 is the whole check. Match the probe interval to the cron cadence: a 5-minute cron is comfortable on a 5-minute probe; a daily cron is comfortable on a slower probe with a generous grace window. The detection lag is bounded by your probe interval, not Railway's cron minimum.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(d) Browser login monitor on the signed-in path.&lt;/strong&gt; The three monitors above prove the platform's pieces are alive. They do not prove a real user can sign in and see their data. The browser login monitor opens a real browser, signs in as a dedicated low-privilege test user, follows the post-login redirect, and asserts the landing page looks right. By default it verifies success by confirming the URL changed; that catches a login that fails outright but not a login that lands on an empty shell because the database read behind it silently failed. Under &lt;strong&gt;Customize detection&lt;/strong&gt;, switch &lt;strong&gt;Success verification&lt;/strong&gt; from the default URL-change to &lt;strong&gt;"Page contains text"&lt;/strong&gt; or &lt;strong&gt;"Element is visible"&lt;/strong&gt;, and set it to a string or selector that only renders when a real database read returned data: a customer name, an invoice ID, a known plan label. This is the clearest case of &lt;a href="https://velprove.com/blog/browser-monitor-vs-http-monitor-decision-tree" rel="noopener noreferrer"&gt;when a browser monitor beats an HTTP probe&lt;/a&gt;. Use a dedicated test account, never real admin credentials. The free plan includes one browser login monitor at a 15-minute interval, which is enough to catch a multi-hour database-backed outage and a login regression inside one window.&lt;/p&gt;

&lt;p&gt;No credit card required. The set lands on free and stays on free unless you want sub-5-minute intervals or more than one browser login monitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest probe-cost tradeoff on Railway
&lt;/h2&gt;

&lt;p&gt;Probes cost request volume on your service, not Railway pricing dollars in this post's frame. The math is easy. A 1-minute probe from a single region hits your endpoint about once per minute, which is 1,440 per day, which is roughly 43,200 requests per month at that single endpoint. A 5-minute cron-heartbeat probe from a single region is about 288 per day, roughly 8,640 per month. Both numbers are small relative to any real traffic, but they are not zero, and they are the load you are adding by deciding to probe continuously.&lt;/p&gt;

&lt;p&gt;The sane default for Railway on the Velprove free plan is HTTP probes at 300-second (5-minute) intervals, which is what the free plan includes. That is enough to catch a multi-minute outage and small enough to stay invisible on any real Railway service's billing. If you need the 1-minute interval, you need it for the customer-facing paths where one minute of detection lag is one minute of silent revenue loss, not for the cron heartbeat that fires hourly anyway.&lt;/p&gt;

&lt;p&gt;The same probe-cost discipline applies across the Platform sibling guides: &lt;a href="https://velprove.com/blog/monitor-render-hosted-app" rel="noopener noreferrer"&gt;Render&lt;/a&gt;, &lt;a href="https://velprove.com/blog/monitor-vercel-hosted-site" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt;, and &lt;a href="https://velprove.com/blog/monitor-cloudflare-workers-pages-site" rel="noopener noreferrer"&gt;Cloudflare Workers and Pages&lt;/a&gt; carry the same four-pattern shape, with platform-specific plumbing under each pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Velprove keep my Railway service from sleeping?
&lt;/h3&gt;

&lt;p&gt;No. Railway's sleep timer is outbound-driven. Per &lt;a href="https://docs.railway.com/reference/app-sleeping" rel="noopener noreferrer"&gt;Railway's app-sleeping docs&lt;/a&gt;, a service goes to sleep when it has not sent outbound traffic for at least 10 minutes, and inbound traffic is explicitly excluded from that decision. A Velprove HTTP probe arrives at your service as inbound traffic, so it does not reset the sleep clock. It will wake a slept service on the first request, then sleep again 10 minutes after your service stops sending outbound traffic. If you need the service awake, do that with outbound activity inside the service, not with an external monitor.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I monitor a Railway service that only runs on &lt;code&gt;railway.internal&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;You cannot reach &lt;code&gt;*.railway.internal&lt;/code&gt; from outside. The private network is a Wireguard mesh scoped to a single project and environment, structurally unreachable from the public internet. The working pattern is a public companion route, for example &lt;code&gt;/deps&lt;/code&gt; on a public web service in the same project, that exercises the internal call and returns 200 only if the private service responded. A Velprove HTTP monitor against &lt;code&gt;/deps&lt;/code&gt;, probing from one of 5 global regions you pick, then tells you when the private service stops answering.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I detect a Railway cron that did not fire?
&lt;/h3&gt;

&lt;p&gt;Heartbeat pattern. The cron writes a timestamp on success into Postgres or a key value store. A small companion web service reads the timestamp, computes its age, and returns 503 when the cron has gone stale, 200 otherwise. A Velprove HTTP monitor asserts &lt;code&gt;status_code = 200&lt;/code&gt; on that endpoint. &lt;a href="https://docs.railway.com/reference/cron-jobs" rel="noopener noreferrer"&gt;Railway's cron docs&lt;/a&gt; state that if a previous execution is still running when the next scheduled run is due, Railway skips the new run, so a hung cron looks identical to a missing cron from outside. Velprove does not receive passive heartbeats, so the freshness lives on your endpoint and Velprove asserts the status code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Railway alert me when a deploy serves the wrong build SHA?
&lt;/h3&gt;

&lt;p&gt;No. Railway's native healthcheck only gates the deploy at activation time, not its content afterwards. Expose &lt;code&gt;/version&lt;/code&gt; returning the git SHA from a build-time environment variable, then assert it with Velprove. The recommended form is a multi-step API monitor: Step 1 captures &lt;code&gt;$.build_sha&lt;/code&gt; from &lt;code&gt;/version&lt;/code&gt; into a variable, Step 2 hits a second route that compares its own runtime SHA against the captured value and returns non-2xx on mismatch. The comparison lives in your app, the monitor just orchestrates, and the setup survives every deploy unchanged. The lighter alternative is a plain HTTP monitor with &lt;code&gt;body_contains&lt;/code&gt; set to the current SHA, but &lt;code&gt;body_contains&lt;/code&gt; goes stale on your next deploy unless your CI/CD updates it via Velprove's &lt;code&gt;PUT /api/checks/&amp;lt;id&amp;gt;&lt;/code&gt; API. When a deploy reports green but serves a stale or wrong build, either assertion fails and the monitor pages you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Railway's native healthcheck enough for uptime monitoring?
&lt;/h3&gt;

&lt;p&gt;No, and Railway says so. &lt;a href="https://docs.railway.com/reference/healthchecks" rel="noopener noreferrer"&gt;The healthchecks reference page&lt;/a&gt; states, verbatim: &lt;em&gt;"The healthcheck endpoint is currently not used for continuous monitoring as it is only called at the start of the deployment, to ensure it is healthy prior to routing traffic to it."&lt;/em&gt; It is a deploy-time gate that lets a new instance start receiving traffic once it returns 200, not a runtime alert that fires when the instance later stops responding. Continuous uptime needs an external probe.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the cheapest way to monitor a Railway app?
&lt;/h3&gt;

&lt;p&gt;The Velprove free plan. It covers 10 monitors total, a 5-minute HTTP interval, one browser login monitor at a 15-minute interval, multi-step API monitors up to 3 steps, email alerts, and 1 status page, with each monitor probing from one of 5 global regions you pick. That is enough to land a public HTTP monitor on your web service, a &lt;code&gt;/deps&lt;/code&gt; monitor on a private dependency, a heartbeat monitor on a cron, and one browser login monitor on the signed-in path. &lt;a href="https://velprove.com/signup" rel="noopener noreferrer"&gt;Start with the free plan&lt;/a&gt;. No credit card required.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>uptime</category>
    </item>
    <item>
      <title>Monitor AI App Uptime When OpenAI or Anthropic Degrades</title>
      <dc:creator>velprove</dc:creator>
      <pubDate>Tue, 19 May 2026 14:00:04 +0000</pubDate>
      <link>https://dev.to/velprove/monitor-ai-app-uptime-when-openai-or-anthropic-degrades-5b54</link>
      <guid>https://dev.to/velprove/monitor-ai-app-uptime-when-openai-or-anthropic-degrades-5b54</guid>
      <description>&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; If your core feature is an AI call, a degraded provider is a degraded product, and a naive ping of &lt;code&gt;api.openai.com&lt;/code&gt; will mostly return 200 while your users watch the feature fail. The check that actually catches this is a multi-step API monitor that signs in and calls your own in-app AI endpoint, asserting the response shape, the HTTP status, and a &lt;code&gt;response_time_ms&lt;/code&gt; budget, paired with a browser login monitor that signs in as a real user and confirms the AI feature actually rendered. Velprove proves your AI endpoint responded fast and in the expected shape. It does not and cannot judge whether the model's answer was good or correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 15 hours OpenAI's API ran at 75%
&lt;/h2&gt;

&lt;p&gt;On June 9 and 10, 2024, OpenAI had an incident that lasted roughly 15.5 hours. It was not a clean outage. According to &lt;a href="https://status.openai.com/incidents/01JXCAW3K3JAE0EP56AEZ7CBG3/write-up" rel="noopener noreferrer"&gt;OpenAI's June 2024 postmortem&lt;/a&gt; , "ChatGPT users experienced elevated error rates reaching ~35% errors at peak, while API users experienced error rates peaking at ~25%," and for the API, "Availability dropped to 75% during the incident." The root cause was mundane: "a daily scheduled system update inadvertently restarted the network management service (systemd-networkd) on affected nodes, causing a conflict with a networking agent."&lt;/p&gt;

&lt;p&gt;Read the 75% number again, because it is the whole point of this post. The API was not down. It was up, and serving correct responses, roughly three times out of four, for fifteen hours. A monitor that asks "is &lt;code&gt;api.openai.com&lt;/code&gt; reachable, does it return 200" would have passed most of the time, because most of the time it genuinely did. Meanwhile an app that calls that API on every user action, without retry-with-jitter, was surfacing roughly one in four AI-feature requests as a failure to real users. The provider endpoint was nominally up. The product was not.&lt;/p&gt;

&lt;p&gt;That gap did not show up at &lt;code&gt;api.openai.com&lt;/code&gt;. It showed up inside your own AI feature, as elevated errors, latency blowout, 429s and 529s, or a stream that returned HTTP 200 and then died mid-completion. The general case of what an HTTP 200 misses is its own subject, covered in &lt;a href="https://velprove.com/blog/why-uptime-monitors-miss-outages" rel="noopener noreferrer"&gt;why uptime monitors miss real outages&lt;/a&gt; . This post is the AI-provider-specific case, and it has failure modes that the general catalogue does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  How an LLM provider actually degrades to your app
&lt;/h2&gt;

&lt;p&gt;When a provider degrades, it does not politely return a single clean error code. Here is the detectable surface, by primary source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency blowout.&lt;/strong&gt; The most common partial degradation is not an error at all. Time-to-first-token climbs, the call still completes, the status is still 200, and your users sit watching a spinner. This is invisible to a status-code check and visible to a latency assertion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP 429, which means two different things.&lt;/strong&gt; This distinction is underused and it matters. A &lt;code&gt;rate_limit_exceeded&lt;/code&gt; 429 means request frequency exceeded your account or tier limit. It is transient; retry with backoff. An &lt;code&gt;insufficient_quota&lt;/code&gt; 429 means billing or credits are exhausted. It is not transient. Retrying never helps, and your AI feature is silently dead until you top up. No provider status page will ever show this, because it is account-scoped, not a provider outage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5xx and timeouts during incidents.&lt;/strong&gt; During the documented OpenAI incidents, traffic returned 500s, 503s, and timeouts. Anthropic's &lt;a href="https://platform.claude.com/docs/en/api/errors" rel="noopener noreferrer"&gt;errors documentation&lt;/a&gt; lists "500 - &lt;code&gt;api_error&lt;/code&gt;" and "504 - &lt;code&gt;timeout_error&lt;/code&gt;" explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anthropic 529, &lt;code&gt;overloaded_error&lt;/code&gt;.&lt;/strong&gt; Anthropic's docs define "529 - &lt;code&gt;overloaded_error&lt;/code&gt; : The API is temporarily overloaded" and warn that "529 errors can occur when APIs experience high traffic across all users. In rare cases, if your organization has a sharp increase in usage, you might see 429 errors because of acceleration limits on the API." The error body shape is &lt;code&gt;{&lt;/code&gt;{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}&lt;code&gt;}&lt;/code&gt; , which is exactly the kind of shape you can assert against.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI-specific one: a stream that errors after a 200.&lt;/strong&gt; Anthropic documents this directly: "When receiving a streaming response over SSE, it's possible that an error can occur after returning a 200 response, in which case error handling wouldn't follow these standard mechanisms." A status-code-only check passes, because the status really was 200, while the user gets a truncated or errored completion. This failure mode does not exist for a static REST endpoint, and it is the reason the rest of this post is not just the silent-outage argument with an LLM example. Providers can also serve a slower or lower-tier path under load; treat that qualitatively, as a latency signal, not as a documented behavior with a name.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Velprove can and cannot see here
&lt;/h2&gt;

&lt;p&gt;This goes before the setup on purpose, because if you misunderstand the boundary, you will build the wrong check and trust it for the wrong thing.&lt;/p&gt;

&lt;p&gt;Velprove can prove your AI endpoint responded, with the expected JSON shape, the expected HTTP status, and within a latency budget. It cannot judge whether the model's answer was &lt;em&gt;good&lt;/em&gt;, &lt;em&gt;correct&lt;/em&gt;, &lt;em&gt;relevant&lt;/em&gt;, or &lt;em&gt;not hallucinated&lt;/em&gt;. It asserts that the AI feature &lt;em&gt;responded correctly-shaped and fast&lt;/em&gt;, not that the answer was &lt;em&gt;right&lt;/em&gt;. Output-quality, evals, and hallucination testing are a different tool category and explicitly out of scope. There is no semantic or answer-quality assertion in the product, and there is no way to construct one. The only assertion types that exist are &lt;code&gt;status_code&lt;/code&gt;, &lt;code&gt;body_contains&lt;/code&gt;, &lt;code&gt;body_not_contains&lt;/code&gt;, &lt;code&gt;json_path&lt;/code&gt;, &lt;code&gt;response_time_ms&lt;/code&gt;, and &lt;code&gt;header_contains&lt;/code&gt;. None of those reads meaning.&lt;/p&gt;

&lt;p&gt;Here is the turn, and it is an honest one. Shape, latency, status, and the stream-error-after-200 case catch the overwhelming majority of provider-degradation incidents anyway, precisely because those failures change the shape, status, or latency of the response, not just its quality. A timeout is a latency failure. A 529 is a status failure. A quota-dead account is a body failure. A stalled stream is a completion-marker failure. The class of failure that a Velprove check genuinely cannot see, a confidently-worded but wrong answer returned fast and in the right shape, is real, but it is an evals problem, and conflating the two is how monitoring tools lose credibility. Velprove will not claim that ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  The monitor: a multi-step API check on your own AI endpoint
&lt;/h2&gt;

&lt;p&gt;The useful check is a Velprove multi-step API monitor pointed at your own AI endpoint, not a ping. It has two steps. Step one authenticates as a dedicated low-privilege synthetic test account. Step two calls your in-app AI endpoint with a fixed synthetic test prompt. Use a generic endpoint shape to make this concrete: &lt;code&gt;POST /api/ai/generate&lt;/code&gt; returning &lt;code&gt;{&lt;/code&gt;{ "answer": "..." }&lt;code&gt;}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On step two, set these success conditions. A &lt;code&gt;status_code&lt;/code&gt; assertion that catches 429, 500, 503, 504, and 529 (do not assert 200-only if your endpoint streams, see the next section). A &lt;code&gt;response_time_ms&lt;/code&gt; threshold sized to your real time-to-first-token budget, because latency blowout is the partial degradation a status check misses entirely. A &lt;code&gt;json_path&lt;/code&gt; assertion with the &lt;code&gt;exists&lt;/code&gt; operator on the &lt;code&gt;answer&lt;/code&gt; field, which catches a 200 wrapped around a body that is missing the answer field entirely, a malformed error-shape response. And two &lt;code&gt;body_not_contains&lt;/code&gt; rules, one for &lt;code&gt;overloaded_error&lt;/code&gt; and one for &lt;code&gt;insufficient_quota&lt;/code&gt;, so the two failures that no provider status page will ever show fail your check loudly.&lt;/p&gt;

&lt;p&gt;I am deliberately not re-explaining how multi-step monitors chain requests, extract a token, and carry it forward. That is its own walkthrough; see &lt;a href="https://velprove.com/blog/multi-step-api-monitoring-guide" rel="noopener noreferrer"&gt;the multi-step API monitoring guide&lt;/a&gt; for the mechanics and come back. The plan math here: the Free plan caps multi-step API monitors at 3 steps, so a 2-step check fits Free comfortably. Starter at 19 dollars lifts the cap to 5 steps and the interval to 1 minute; Pro at 49 dollars goes to 10 steps and a 30-second interval. Each monitor runs from one of 5 global regions; if you want regional coverage, run a separate monitor per region, because providers can degrade asymmetrically by region.&lt;/p&gt;

&lt;p&gt;This is the same pattern our &lt;a href="https://velprove.com/blog/monitor-stripe-api-health" rel="noopener noreferrer"&gt;guide to monitoring Stripe API health&lt;/a&gt; applies to your payment provider: your app depends on a third-party API that degrades in ways the vendor status page does not show, so you monitor your own integration point synthetically rather than trusting the dependency to tell you. The dependency is different. The pattern is the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stream that returns 200 and then dies
&lt;/h2&gt;

&lt;p&gt;If your AI endpoint streams, a &lt;code&gt;status_code&lt;/code&gt; assertion alone is not enough, and Anthropic's own docs are why. An error can occur after a 200 has already been returned. The HTTP status was 200. It was honestly 200. The stream then errored, stalled, or truncated, and your user got half an answer or a broken one. A monitor that only checks the status code records a pass and tells you everything is fine.&lt;/p&gt;

&lt;p&gt;The fix is to assert on a stable end-of-stream marker, not on the status. If your endpoint emits a final structured event or sets a completion field once the full answer is assembled, assert it with a &lt;code&gt;json_path&lt;/code&gt; or &lt;code&gt;body_contains&lt;/code&gt; rule: for example, a &lt;code&gt;done: true&lt;/code&gt; field, or a sentinel token your server only writes after the stream closes cleanly. A 200-then-broken stream will not contain that marker, so the check fails on exactly the failure a status-code check waves through. This is the single beat that separates monitoring an AI feature from monitoring any other endpoint, and it is the reason the boundary section above is honest rather than defensive: this failure changes the response shape, so Velprove can see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser login monitor: the AI feature as a signed-in user
&lt;/h2&gt;

&lt;p&gt;The API check proves the endpoint answers correctly-shaped and fast. It does not prove a signed-in user can actually use the AI feature through your real interface, and that is where Velprove's strongest differentiator lives. A browser login monitor opens a real browser, signs in as the dedicated low-privilege test account, and verifies the post-login page rendered correctly. To make it watch the AI feature, point its login URL at an account whose post-login landing surfaces AI-dependent content, then open Customize detection and set Success verification to Page contains text, matching a string that only renders when a real AI result actually loaded. By default this monitor only checks that the URL changed after login, which would pass even if the AI content never rendered, so the default is not enough here. One honest limit: the browser login monitor logs in and checks a single success condition on the resulting page. It does not script clicking into a feature and submitting a prompt. Driving the AI endpoint itself is the multi-step API monitor's job, above.&lt;/p&gt;

&lt;p&gt;This catches failures the API check structurally cannot. A front-end that swallows a 500 and shows a generic toast. A spinner that never resolves because the stream stalled client-side. An auth-gated AI route that the API check authenticated into directly but a real browser session cannot reach because a session or CSRF step broke. A client-side error boundary that renders an empty panel while the network tab shows a clean 200. The API monitor sees a healthy endpoint; the user sees a dead feature; the browser login monitor sees what the user sees.&lt;/p&gt;

&lt;p&gt;Free includes 1 browser login monitor at a 15-minute interval, which is enough to catch a multi-hour provider degradation and a UI regression within one window. Starter includes 3 at a 10-minute interval, and Pro 10 at a 5-minute interval. Point it at the dedicated test account with the smallest permissions that still renders a real AI result, and use a fixed synthetic prompt, never real user data and never a prod-mutating or expensive call, because it runs on every interval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your provider's status page is not the monitor
&lt;/h2&gt;

&lt;p&gt;Start with the argument that is fully sourced and not a matter of timing at all. An OpenAI &lt;code&gt;insufficient_quota&lt;/code&gt; 429 and a per-account acceleration-limit 429 will never appear on status.openai.com or status.anthropic.com, because they are not provider outages. They are account-scoped. The provider is fine. Your account is out of credits or over an acceleration limit, and a synthetic monitor of your own AI endpoint is the only thing that catches them, because there is no public incident to subscribe to.&lt;/p&gt;

&lt;p&gt;Then the timing argument, kept qualitative, because no defensible minute-count exists. OpenAI's &lt;a href="https://status.openai.com/incidents/ctrsv3lwd797" rel="noopener noreferrer"&gt;December 11, 2024 postmortem&lt;/a&gt; describes a control-plane cascade: from 3:16 PM PST to 7:38 PM PST, about 4 hours 22 minutes, after "a new telemetry service deployment that unintentionally overwhelmed the Kubernetes control plane." DNS caching held stale-but-working records for a while, which delayed when services visibly started failing, and OpenAI states plainly that "Remediation was very slow because of the locked out effect." You do not need an invented number to take the point: impact and provider-side acknowledgement and recovery are not the same clock. A monitor of your own endpoint runs on the impact clock. The status page runs on the acknowledgement clock.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is not a substitute for error tracking or evals
&lt;/h2&gt;

&lt;p&gt;One last honest boundary, because credibility is the only thing this post is selling. Velprove tells you fast that your AI endpoint stopped responding correctly-shaped, fast, and with the right status. It does not replace application error tracking, which owns the stack traces and the per-request diagnostics when you go to fix the failure. It does not replace model-output evals, which own whether the answers are actually any good. Three different layers, three different jobs. A synthetic uptime monitor is the layer that tells you the AI feature is failing for users right now, which is the layer most teams launching AI features do not have wired and the one a degraded provider exposes first. Keep the eval suite. Keep the error tracker. Add the monitor that watches the endpoint the way a user hits it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can Velprove tell me if the AI gave a wrong or bad answer?
&lt;/h3&gt;

&lt;p&gt;No. Velprove asserts that your AI endpoint responded, in the expected JSON shape, with the expected HTTP status, inside a latency budget. It does not judge whether the answer was correct, relevant, or hallucinated. That is an evals and output-quality tool category, and it is explicitly out of scope. What Velprove does instead is catch the failure modes that change the shape, status, or latency of the response: timeouts, 429, 500, 503, Anthropic 529, a response missing the answer field, and a stream that returned 200 and then died. Those cover the overwhelming majority of provider-degradation incidents.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I monitor my AI feature without sending real user data to the model?
&lt;/h3&gt;

&lt;p&gt;Use a dedicated low-privilege synthetic test account and a fixed synthetic test prompt that you control. Never send real user data through the monitor, and never point it at a prod-mutating or expensive AI call. The prompt should be short, deterministic in shape, and cheap, because it runs on every probe interval. The point of the check is to prove the endpoint responds correctly-shaped and fast, not to exercise real customer content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will an uptime monitor catch an OpenAI or Anthropic outage before their status page does?
&lt;/h3&gt;

&lt;p&gt;It catches the class of failures a provider status page structurally cannot show, because some of them are account-scoped, not provider outages. An OpenAI &lt;code&gt;insufficient_quota&lt;/code&gt; 429 or a per-account acceleration-limit 429 will never appear on status.openai.com or status.anthropic.com because they are not platform incidents. A synthetic monitor of your own AI endpoint sees the impact where it actually lands, at your request, without waiting for the provider to detect, confirm, and post. OpenAI's own December 11 2024 postmortem describes remediation that was very slow because of the locked out effect, which is a sourced way of saying impact and acknowledgement are not the same clock.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does a 429 from OpenAI actually mean for my app?
&lt;/h3&gt;

&lt;p&gt;Two different things. A &lt;code&gt;rate_limit_exceeded&lt;/code&gt; 429 means request frequency exceeded your account or tier limit. It is transient, and retrying with backoff is the right response. An &lt;code&gt;insufficient_quota&lt;/code&gt; 429 means billing or credits are exhausted. It is not transient. Retrying never helps, and no provider status page will ever show it because it is account-scoped. Assert &lt;code&gt;body_not_contains&lt;/code&gt; on &lt;code&gt;insufficient_quota&lt;/code&gt; so a quota-dead AI feature fails the check loudly instead of degrading silently until a customer notices.&lt;/p&gt;

&lt;h3&gt;
  
  
  My AI endpoint returns 200 but the answer is cut off. Why doesn't my monitor catch it?
&lt;/h3&gt;

&lt;p&gt;Streaming responses can return HTTP 200 and then error mid-stream. Anthropic documents this directly: when receiving a streaming response over SSE, an error can occur after a 200 response has already been returned. A status-code-only assertion passes because the status really was 200. Assert a stable end-of-stream or completion marker with a &lt;code&gt;json_path&lt;/code&gt; or &lt;code&gt;body_contains&lt;/code&gt; rule so a 200-then-broken-stream still fails the check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I do this on the free plan?
&lt;/h3&gt;

&lt;p&gt;Yes. A 2-step API monitor (authenticate, then call your AI endpoint) fits the Free plan, which caps multi-step API monitors at 3 steps. Free also includes 1 browser login monitor at a 15-minute interval and email alerts, with a 5-minute HTTP interval and commercial use allowed. Starter at 19 dollars lifts multi-step to 5 steps, drops the interval to 1 minute, and adds Slack, Discord, Teams, and webhook alerts. Pro at 49 dollars goes to 10 steps and a 30-second interval. &lt;a href="https://velprove.com/signup" rel="noopener noreferrer"&gt;Start with the free plan&lt;/a&gt;. No credit card required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which region does the AI endpoint check run from?
&lt;/h3&gt;

&lt;p&gt;From any one of 5 global regions. Each monitor runs from a single region you pick, not all of them at once. If you want regional coverage of your AI endpoint, create separate monitors per region. This matters for AI features because a provider can degrade asymmetrically by region, and a single-region monitor only sees its own region's path.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>uptime</category>
    </item>
    <item>
      <title>Detecting a Hacked WordPress Site: Skimmers and Silent Defacement</title>
      <dc:creator>velprove</dc:creator>
      <pubDate>Tue, 19 May 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/velprove/detecting-a-hacked-wordpress-site-skimmers-and-silent-defacement-5ak0</link>
      <guid>https://dev.to/velprove/detecting-a-hacked-wordpress-site-skimmers-and-silent-defacement-5ak0</guid>
      <description>&lt;p&gt;&lt;strong&gt;The honest take:&lt;/strong&gt; when an attacker injects a card skimmer, defaces a page, or hijacks your wp-admin, your host dashboard keeps returning a green HTTP 200, because the site is still being served, it is just serving the attacker's version. Velprove is not a security scanner and has no server access. Its browser login monitor signs in to your wp-admin in a real browser, and content assertions check the page a visitor actually loads, so it can flag that your expected checkout or admin markup is gone, or that a known bad marker appeared, as a fast external tripwire. It is complementary to server-side tools like Wordfence or Sucuri, not a replacement for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A card skimmer ran on a live store for weeks, and the uptime dashboard never blinked
&lt;/h2&gt;

&lt;p&gt;In September 2024, Sucuri documented a WooCommerce credit card skimmer with a detail worth sitting with. As Sucuri put it in their &lt;a href="https://blog.sucuri.net/2024/09/woo-skimmer-uses-style-tags-and-image-extension-to-steal-card-details.html" rel="noopener noreferrer"&gt;September 12, 2024 writeup&lt;/a&gt; , "All the attackers did was simply edit the checkout page source, either from wp-admin (using a compromised administrator user) or directly through the database." The payload was not even a conspicuous &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag. It hid inside a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag and executed through an &lt;code&gt;onload&lt;/code&gt; handler once the page finished loading, with the skimmer body heavily obfuscated through custom character substitution and shuffling.&lt;/p&gt;

&lt;p&gt;Now hold that next to what every availability check saw. The store was up. The checkout page returned a 200. The cart worked, the product pages loaded, the host status panel was green. A skimmer that quietly copies every card number a customer types can run for weeks this way, because nothing about availability changes: the site stays up and keeps returning 200 while the attacker collects card data on every order. If you run a store, the natural next step is dedicated &lt;a href="https://velprove.com/blog/monitor-woocommerce-checkout" rel="noopener noreferrer"&gt;WooCommerce checkout monitoring&lt;/a&gt; , and this post is about the adversarial half of that: not a checkout that broke, a checkout an attacker quietly rewrote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an HTTP 200 dashboard is structurally blind to this
&lt;/h2&gt;

&lt;p&gt;A status-code check asks one question: did the server answer? A compromised site answers fine. It returns a 200 with a fully rendered page, because the page is doing exactly what the attacker wants it to do. This is not the familiar "200 but the page is blank" problem where a build failed and the body is empty. This is a 200 serving a page an attacker now controls: the markup is present, it renders, it just contains a skimmer or a defacement or a login that now belongs to someone else. Availability monitoring is the wrong instrument for it, the same structural reason &lt;a href="https://velprove.com/blog/why-uptime-monitors-miss-outages" rel="noopener noreferrer"&gt;uptime monitors miss outages like this&lt;/a&gt; . The fix is not a better status-code check. It is checking the content of the page a visitor actually loads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three ways a compromise shows up on a page a visitor can see
&lt;/h2&gt;

&lt;p&gt;This post is about your site being compromised by an attacker, an injected skimmer, a silent defacement, a hijacked admin, not about &lt;a href="https://velprove.com/blog/wordpress-plugin-update-broke-site-monitoring" rel="noopener noreferrer"&gt;a legitimate plugin update breaking your own site, which is a different problem&lt;/a&gt; . That sibling is about your site breaking itself when a good-faith update throws a fatal. This one is about the page doing precisely what an attacker intends while your host dashboard shows a green 200. Three patterns dominate the externally visible side of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Injected scripts and skimmers.&lt;/strong&gt; The largest example on record is Balada Injector. Sucuri, in their &lt;a href="https://blog.sucuri.net/2023/04/balada-injector-synopsis-of-a-massive-ongoing-wordpress-malware-campaign.html" rel="noopener noreferrer"&gt;campaign synopsis&lt;/a&gt; , estimated that "since 2017, we estimate that over one million WordPress websites have been infected by this campaign," and noted it "consistently ranks in the top 3 of the infections that we detect and clean." It typically enters through a vulnerable plugin: BleepingComputer &lt;a href="https://www.bleepingcomputer.com/news/security/over-17-000-wordpress-sites-hacked-in-balada-injector-attacks-last-month/" rel="noopener noreferrer"&gt;reported&lt;/a&gt; that Sucuri detected it on over 17,000 WordPress sites in September 2023, more than 9,000 of them through one plugin XSS flaw. The injected code redirects visitors and adds backdoors. To a visitor it is a script that should not be there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent defacement.&lt;/strong&gt; The content or appearance of a page is changed without the site going down. A pricing page now reads differently, a banner appears that you did not put there, a section is replaced. The page still returns 200 and renders cleanly. Nothing about it is "down." It is just no longer your page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hijacked wp-admin.&lt;/strong&gt; In Sucuri's &lt;a href="https://blog.sucuri.net/2024/06/2023-hacked-website-malware-threat-report.html" rel="noopener noreferrer"&gt;2023 hacked-website report&lt;/a&gt; , among the sites Sucuri remediated, "malicious WordPress admin users were found in 55.2% of infected databases," and SEO spam appeared on 42.22% of infected sites. That is a remediation-sample figure, not a rate across all WordPress sites, but the direction is clear: when an attacker gets in, control of the admin is a common outcome, and the externally visible consequence is a wp-admin login that no longer behaves the way it should.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Velprove is not, and read this before you set anything up
&lt;/h2&gt;

&lt;p&gt;This is the part that keeps this post honest, so it goes before the setup, not after it. Velprove is not a security scanner and it has no server access. It does not read your files, it does not scan for malware, it does not do file-integrity monitoring, and it is not a web application firewall or a vulnerability scanner. It sees exactly one thing: the page a visitor's browser receives from the outside.&lt;/p&gt;

&lt;p&gt;That means Velprove detects the symptom, not the cause. It can tell you the expected checkout markup vanished, a known injected marker is present, or the wp-admin login is broken. It cannot tell you which plugin was vulnerable, which file was modified, or that a rogue admin row was written to the database. Server-side tools like Wordfence and Sucuri do that work, scanning files and doing integrity monitoring on the server itself. Velprove is the fast external tripwire that fires from outside the host you are trying to verify. The two are complementary, and the rest of this post is written on that understanding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: a positive DOM tripwire, a known-bad check, and a wp-admin login monitor
&lt;/h2&gt;

&lt;p&gt;Three monitors cover the externally visible surface of a compromise. The first is the lead, and it is the one almost no uptime tool gives you on a free plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A browser login monitor on wp-admin&lt;/strong&gt;. This is the strongest leg, and it is the differentiator. Velprove opens a real browser, navigates to your &lt;code&gt;wp-login.php&lt;/code&gt;, signs in as a dedicated low-privilege test account, and asserts that the expected post-login admin markup rendered. When an attacker changes credentials, locks accounts out, or replaces the login flow, this monitor fails, which is the externally visible face of the hijacked-admin pattern and the 55.2% figure above. The mechanics are walked through in detail in &lt;a href="https://velprove.com/blog/monitor-wordpress-login" rel="noopener noreferrer"&gt;the wp-admin browser login monitor guide&lt;/a&gt; . Use a dedicated low-privilege test account, never your real administrator credentials. One configuration detail here is load-bearing: in the monitor's Customize detection options, set Success verification to Page contains text and point it at a stable string only the real admin dashboard renders. Leave it on the default URL-change check, and a hijacked login that still redirects can pass while a visitor is seeing the attacker's page. The post-login markup assertion is what makes this monitor detect the symptom, not just that some redirect happened. &lt;strong&gt;A positive body_contains tripwire&lt;/strong&gt;. Add an HTTP monitor with a &lt;code&gt;body_contains&lt;/code&gt; assertion on a stable piece of markup that must be present on a healthy page: a checkout form field name, an admin shell element, a distinctive footer string. If a defacement or a skimmer rewrites the page, that expected markup is the first thing to disappear, and the assertion fails. This is the robust play, because it does not require knowing the attacker's payload in advance. &lt;strong&gt;A targeted body_not_contains check&lt;/strong&gt;. Add a &lt;code&gt;body_not_contains&lt;/code&gt; assertion on a specific known bad string only when you or your security tool have already identified one: a specific injected script source, a known exfiltration host, a known defacement banner string. This is a narrower secondary layer, not the primary defense.&lt;/p&gt;

&lt;p&gt;One product fact to set expectations correctly: each Velprove monitor probes from a single region. You can choose which of the 5 global regions runs a given monitor, or run separate monitors per region if you want multi-region coverage. There is no "every check from all regions at once."&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limitation: a fixed substring cannot catch a rotating skimmer
&lt;/h2&gt;

&lt;p&gt;Velprove's assertions are fixed substring matches, not regular expressions or pattern matching. &lt;code&gt;body_contains&lt;/code&gt; checks whether an exact string is present, and &lt;code&gt;body_not_contains&lt;/code&gt; checks whether an exact string is absent. That has a direct consequence you should know before you rely on it. Modern skimmers, as the Sucuri Woo skimmer writeup showed, obfuscate their payload with custom character substitution and rotate their exfiltration domains. A &lt;code&gt;body_not_contains&lt;/code&gt; assertion catches only a known fixed string. The moment the attacker re-obfuscates or rotates, that exact string changes and the known-bad check goes silent.&lt;/p&gt;

&lt;p&gt;This is why the positive tripwire leads and the known-bad check is secondary. Asserting that your expected checkout or admin markup is present does not depend on predicting the attacker's payload. Most page replacements and many injection techniques disturb the legitimate markup, so a positive-presence assertion is the durable signal. The honest framing is: Velprove reliably tells you the expected page is no longer intact, and it can catch a specific known marker you already know about, but it is not a promise to catch every obfuscated or rotating payload. Anyone selling you that promise from the outside of your server is overclaiming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits next to Wordfence and Sucuri
&lt;/h2&gt;

&lt;p&gt;Wordfence and Sucuri are dedicated WordPress security platforms. They do server-side malware scanning and file-integrity monitoring: they read the files on your server and tell you when one changed in a way it should not have. That is real, important work, and Velprove does not do it and does not claim to.&lt;/p&gt;

&lt;p&gt;Velprove sits in a different and narrower place. It watches the rendered page from outside the server, with no plugin and no server access, and fires fast when the page a visitor loads stops looking like your page. A server-side scanner answers "did a file on my server change?" Velprove answers "did the page my customer sees change?" Those are not the same question, and a real compromise often trips one before the other. The right posture is both: a server-side scanner for file and integrity coverage, an external content tripwire so you hear about the visible symptom quickly even from a network position the attacker does not control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set this up in the next ten minutes, free
&lt;/h2&gt;

&lt;p&gt;You do not need a paid plan to put this in place. Velprove's free plan includes 10 monitors, one browser login monitor at a 15-minute interval, a 5-minute HTTP interval, email alerts, and a choice of 5 global regions, with no credit card required. That is enough for the wp-admin browser login monitor plus the &lt;code&gt;body_contains&lt;/code&gt; positive tripwire and a targeted &lt;code&gt;body_not_contains&lt;/code&gt; check on the same page.&lt;/p&gt;

&lt;p&gt;The browser login monitor is the leg to set up first, because a hijacked admin is both common and the hardest of the three to notice on your own. Point it at your &lt;code&gt;wp-login.php&lt;/code&gt;, sign in with a dedicated low-privilege test account, and let it run. Then layer the positive content tripwire on your checkout or a high-value page. None of it touches your server, and all of it runs from outside the host you are trying to trust. Pair it with a server-side scanner and you have covered both the file and the page. &lt;a href="https://velprove.com/signup" rel="noopener noreferrer"&gt;Start with the free plan&lt;/a&gt;. No credit card required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I detect if my WordPress site has been hacked?
&lt;/h3&gt;

&lt;p&gt;From the outside, you watch the symptom on the page a real visitor loads: the expected checkout or admin markup is missing, a known injected marker appeared, or the wp-admin login no longer works. Velprove does this with a &lt;code&gt;body_contains&lt;/code&gt; assertion on the markup that should be present, a &lt;code&gt;body_not_contains&lt;/code&gt; assertion on a known bad string, and a browser login monitor that signs in to wp-admin. That is symptom detection. It does not replace a server-side scanner that reads your files for malware and file-integrity changes. Run both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Velprove need a plugin or server access to detect a compromise?
&lt;/h3&gt;

&lt;p&gt;No. Velprove is not a security scanner and has no access to your server, files, or database. It reads only the externally rendered page, the same one a visitor's browser receives. It cannot scan files for malware or do file-integrity monitoring, and it asks for nothing to be installed on the site. That boundary is the point: it is a fast external tripwire that runs independently of the host you are trying to verify, and it is complementary to, not a replacement for, a server-side security tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can an uptime monitor catch a credit card skimmer on my WooCommerce checkout?
&lt;/h3&gt;

&lt;p&gt;It can catch the symptom, with one honest caveat. A &lt;code&gt;body_contains&lt;/code&gt; assertion that the expected checkout form markup is intact is the robust play, because a skimmer that tampers with the checkout often disturbs that markup. A &lt;code&gt;body_not_contains&lt;/code&gt; assertion on a specific known bad string catches that exact string. The caveat: assertions are fixed substring matches, not patterns, and modern skimmers obfuscate and rotate their payload, so the positive presence check is the durable one and the known-bad check is a targeted secondary, not a promise to catch every variant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my uptime monitor still show green when my site is hacked?
&lt;/h3&gt;

&lt;p&gt;Because the site is still being served. A status-code check asks whether the server answered, and a compromised site answers fine: it returns a 200 with a fully rendered page. The page is just doing exactly what the attacker wants. The skimmer collects cards, the defaced content is live, the hijacked admin is theirs, and every layer that only watches availability reports healthy. You need a check that inspects the content of the page a visitor actually loads, not just the status line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Velprove a replacement for Wordfence or Sucuri?
&lt;/h3&gt;

&lt;p&gt;No, and it is not trying to be. Wordfence and Sucuri are server-side WordPress security platforms that scan your files for malware and do file-integrity monitoring on the server. Velprove has no server access and does none of that. It watches the rendered page from the outside as a fast external tripwire. The two answer different questions: a server-side scanner tells you a file changed, Velprove tells you the page a visitor sees changed. Use both. They cover different layers of the same problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should I assert to detect a defaced or skimmed page?
&lt;/h3&gt;

&lt;p&gt;Lead with a positive tripwire. In Velprove, use a &lt;code&gt;body_contains&lt;/code&gt; assertion on a stable piece of markup that must be present on a healthy page: a checkout form field name, an admin shell element, a footer string the attacker is unlikely to preserve when they replace the page. Then add a &lt;code&gt;body_not_contains&lt;/code&gt; assertion on a specific known bad string only if you or your security tool have already identified one. The positive check is more robust because it does not depend on knowing the attacker's exact payload in advance, which with obfuscated and rotating skimmers you usually do not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Velprove tell me if someone created a fake admin account?
&lt;/h3&gt;

&lt;p&gt;Not directly, and being honest about that matters. Velprove has no server access and cannot read your WordPress user table, so it cannot tell you a rogue admin row exists. What it can catch is the symptom: a browser login monitor that signs in to wp-admin with a dedicated low-privilege test account will fail when an attacker has changed credentials, locked accounts out, or replaced the login flow. That is the externally visible consequence of a hijacked admin, not the database state itself. For the underlying account audit you still need a server-side tool.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
      <category>uptime</category>
    </item>
  </channel>
</rss>
