[{"data":1,"prerenderedAt":42},["ShallowReactive",2],{"blog-post-why-i-run-uptime-checks-at-the-edge":3},{"slug":4,"title":5,"description":6,"date":7,"updated":8,"tags":9,"pillar":15,"draft":16,"image":17,"imageAlt":18,"html":19,"markdown":20,"faq":21,"readingTimeText":40,"readingTimeMinutes":41},"why-i-run-uptime-checks-at-the-edge","Why I run uptime checks at the edge, not one server","A single VPS in one region tells you about its own path, not your users'. Here is how I built edge uptime checks on Cloudflare Workers and why.","2026-04-29T00:00:00.000Z",null,[10,11,12,13,14],"uptime-monitoring","cloudflare-workers","edge","architecture","reliability","revenue-critical-code",false,"/blog-covers/revenue-critical-code.png","Revenue-critical code cover","\u003Cdiv class=\"post-tldr\">\u003Cdiv class=\"post-tldr-label\">TL;DR\u003C/div>\u003Cp>I run uptime checks as Cloudflare Workers on a cron, not a daemon in one region, because one probe only reveals its own path, not your users’. The edge kills the babysat box, but true multi-vantage checking is deliberate design on top.\u003C/p>\u003C/div>\n\u003Cp>I run my uptime monitoring platform as Cloudflare Workers on a cron trigger, not as a daemon on a box in us-east-1. The reason is simple: one server in one region does not tell you whether your site is down, it tells you whether that server’s path to your site is down. Those are different questions, and conflating them produces the two failures every monitoring tool is judged on: false alarms at 2am and missed real outages. Moving the checker to the edge changes the economics and the correctness at the same time. Here is the actual design.\u003C/p>\n\u003Ch2 id=\"the-problem-with-one-probe\" tabindex=\"-1\">The problem with one probe\u003C/h2>\n\u003Cp>A monitor that runs from a single region has a blind spot the size of that region. If your one probe sits in us-east-1 and us-east-1 has a bad afternoon, every site you watch goes “down” at once, and you page a hundred customers about an outage that is yours, not theirs. The inverse is worse: a routing problem between your probe and a target makes a perfectly healthy site look dead, and now the customer stops trusting your alerts, which is the only thing a monitoring product sells.\u003C/p>\n\u003Cp>You also inherit an always-on server to babysit. It needs patching, it has a fixed cost whether it checks ten monitors or ten thousand, and when it dies your monitoring dies silently, which is the one component that must never fail quietly.\u003C/p>\n\u003Ch2 id=\"why-workers%2C-concretely\" tabindex=\"-1\">Why Workers, concretely\u003C/h2>\n\u003Cp>Cloudflare Workers on a cron trigger removes the box. My checker is a scheduled Worker that fires every minute:\u003C/p>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan># wrangler.toml\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>[triggers]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>crons = [\"* * * * *\", \"5 0 * * *\"]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>The first schedule runs the checks every minute. The second (\u003Ccode>5 0 * * *\u003C/code>, just after midnight) runs daily rollups and cleanup, and only on one shard so the heavy job runs once, not once per shard. There is no server to keep alive. Cloudflare invokes the Worker; if a given invocation fails, the next one is sixty seconds away. The thing that watches everyone else does not have a single always-on point of failure that I have to monitor with yet another tool.\u003C/p>\n\u003Cp>The cost model flips too. An always-on VPS bills for time it sits idle. Workers bill per invocation and per unit of work, so a minute where nothing is due to be checked costs almost nothing. That is the right shape for a workload that is bursty by nature: most monitors are not due most minutes.\u003C/p>\n\u003Ch2 id=\"sharding%2C-because-a-minute-is-a-hard-deadline\" tabindex=\"-1\">Sharding, because a minute is a hard deadline\u003C/h2>\n\u003Cp>Here is a constraint people miss. If checks run every minute, every batch has to finish inside a minute, including retries and database writes. As the number of monitors grows, one Worker invocation cannot fan out to all of them and still land in time. So the checker is sharded, and the shard math lives in the query that selects work:\u003C/p>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">const\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> shard\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> parseInt\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(env.\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">SHARD\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ||\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '0'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">const\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> totalShards\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> parseInt\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(env.\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">TOTAL_SHARDS\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ||\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '1'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">// each shard owns a disjoint slice of monitors by id\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">// ... WHERE m.id % ? = ?  bound to (totalShards, shard)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Each deployed Worker owns \u003Ccode>m.id % totalShards == shard\u003C/code>, a disjoint slice of monitors, so two shards never check the same monitor and together they cover all of them. Adding capacity is deploying another shard and bumping \u003Ccode>TOTAL_SHARDS\u003C/code>, not resizing a box. The slice is deterministic, so a monitor always lands on the same shard, which keeps its history coherent.\u003C/p>\n\u003Ch2 id=\"fan-out-with-a-hard-timeout-on-every-check\" tabindex=\"-1\">Fan-out with a hard timeout on every check\u003C/h2>\n\u003Cp>Inside a shard, checks run concurrently, and each one has its own abort timeout so a single slow target cannot eat the whole minute:\u003C/p>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">const\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> controller\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> AbortController\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">const\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> timer\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> setTimeout\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(() \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> controller.\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">abort\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(), monitor.timeout \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">*\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1000\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">try\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> response\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(monitor.url, { signal: controller.signal, redirect: \u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'follow'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">  clearTimeout\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(timer)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">  // status compared against monitor.expected_status\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">} \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">catch\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (err) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">  clearTimeout\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(timer)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">  // AbortError becomes 'Request timeout', everything else keeps its message\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>The whole batch is dispatched with \u003Ccode>Promise.allSettled\u003C/code>, not \u003Ccode>Promise.all\u003C/code>, on purpose. One monitor throwing must not sink the results for every other monitor in the shard. \u003Ccode>allSettled\u003C/code> gives me every outcome, good and bad, and I record them all. \u003Ccode>all\u003C/code> would reject on the first failure and throw away the work already done, which in a monitoring system means losing the very signal you exist to capture.\u003C/p>\n\u003Ch2 id=\"not-every-blip-is-an-outage%3A-confirm-before-you-page\" tabindex=\"-1\">Not every blip is an outage: confirm before you page\u003C/h2>\n\u003Cp>The edge buys location, but correctness still needs discipline in the logic, and this is where most homegrown monitors alarm too eagerly. A single failed request is not an incident. Networks hiccup. So there are two gates before anyone gets paged.\u003C/p>\n\u003Cp>First, an immediate retry when a check comes back down:\u003C/p>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">let\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> result \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> doFetch\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(monitor, headers)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">if\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (result.status \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">===\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> 'down'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  await\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Promise\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">r\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> setTimeout\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(r, \u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">2000\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) \u003C/span>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">// wait 2s, then look again\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> retry\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> doFetch\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(monitor, headers)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (retry.status \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">===\u003C/span>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> 'up'\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) result \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> retry\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Second, an incident only opens when the current check is down and the previous stored check was also down. One down check is noise; two in a row is a signal. A recovery (an \u003Ccode>up\u003C/code> check while an incident is open) resolves it and notifies once. That two-strikes rule, plus the inline retry, is what keeps the platform from crying wolf on a single dropped packet. It is the same instinct that keeps \u003Ca href=\"/blog/six-payments-edge-cases-that-bite-at-2am\">payment retries from stampeding a struggling gateway\u003C/a>: confirm, back off, and do not react to a single blip.\u003C/p>\n\u003Cp>A monitor spends its credibility every time it pages on nothing, and it never fully earns that trust back. So I would rather lose the first few seconds of a real outage to a confirmation step than wake someone for a single dropped packet, because an alert nobody believes is worse than no alert at all.\u003C/p>\n\u003Ch2 id=\"the-database-has-to-be-near-the-checker-too\" tabindex=\"-1\">The database has to be near the checker too\u003C/h2>\n\u003Cp>If the checker is at the edge but the database is a single Postgres box in one region, you have just moved the single point of failure and added latency to every write. Each check writes a row, and there are a lot of checks per minute. So the store is Cloudflare D1 (SQLite at the edge), bound straight into the Worker. Writes are batched in chunks so a shard with many results does not fire hundreds of individual statements inside its one-minute window:\u003C/p>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">for\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">let\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> i \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">; i \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> batch.\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">length\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">; i \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+=\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 100\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  await\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> db.\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">batch\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(batch.\u003C/span>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">slice\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(i, i \u003C/span>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C/span>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 100\u003C/span>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">))\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Keeping compute and storage in the same platform is not a purity thing. It is what makes the per-minute deadline achievable at all.\u003C/p>\n\u003Ch2 id=\"what-the-edge-does-not-magically-give-you\" tabindex=\"-1\">What the edge does not magically give you\u003C/h2>\n\u003Cp>I will be honest about the limit, because overclaiming is how monitoring tools lose trust. Running as a Worker means checks originate from Cloudflare’s network rather than a fixed VPS, and it removes the babysat server. It does not, by itself, guarantee that every check leaves from a different continent every minute; Cloudflare decides where a cron invocation runs. True multi-vantage checking is a deliberate design on top of this (invoking from chosen locations and reconciling their verdicts), not a free side effect of the word “edge.” What the architecture does guarantee is that I am not betting my customers’ trust on the health of one region and one always-on machine.\u003C/p>\n\u003Cp>That is the trade I wanted. No server to keep breathing, a per-minute deadline I can actually hit by sharding, a store that lives next to the compute, and alerting logic that confirms before it wakes anyone. A monitoring tool earns its keep on the night it stays quiet because it correctly decided a single dropped packet was not the end of the world. That quiet is what \u003Ca href=\"/blog/boring-code-is-a-feature-you-ship-to-your-future-self\">boring, durable code buys you\u003C/a>: no drama, no heroics, just a system that keeps deciding correctly while you sleep.\u003C/p>\n\u003Cdiv class=\"post-takeaway\">\u003Cdiv class=\"post-takeaway-label\">The takeaway\u003C/div>\u003Cp>Moving the checker to the edge is not about the word edge, it is about removing the two things that make a single-region monitor lie: a blind spot the size of one region and an always-on box that fails silently. Workers give me a per-invocation cost model, sharding that hits the one-minute deadline, storage in D1 next to the compute, and a confirm-before-you-page rule that keeps the alerts trustworthy. I still owe the deliberate work of true multi-vantage checking, but I am no longer betting my customers’ trust on the health of one region and one machine.\u003C/p>\u003C/div>\u003Ch2 id=\"faq\" tabindex=\"-1\">FAQ\u003C/h2>\n\u003Cdiv class=\"post-faq\">\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">Why not just run the monitor from one VPS in us-east-1?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>Because a probe in one region only tells you whether that region’s path to your site is healthy, so a bad afternoon in us-east-1 pages every customer at once, and a routing problem makes a healthy site look dead. Both failures destroy the trust that is the only thing a monitoring product sells.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">Why Cloudflare Workers on a cron trigger instead of a daemon?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>Workers remove the always-on server you have to patch and babysit, and they bill per invocation instead of for idle time, which fits a bursty workload where most monitors are not due most minutes. There is no single always-on point of failure watching everyone else.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">Why shard the checker?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>Because checks run every minute and each batch, including retries and writes, must finish inside that minute. One invocation cannot fan out to every monitor as the count grows, so each shard owns a disjoint slice by monitor id, and adding capacity is deploying another shard, not resizing a box.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">Why Promise.allSettled instead of Promise.all for the fan-out?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>Because one monitor throwing must not sink the results for every other monitor in the shard. allSettled records every outcome, good and bad, while all rejects on the first failure and throws away work already done, which in monitoring means losing the signal you exist to capture.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">How do you avoid paging on a single dropped packet?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>Two gates: an inline retry two seconds after a down result, and an incident that only opens when the current check and the previous stored check were both down. One down check is noise; two in a row is a signal, and a recovery resolves and notifies once.\u003C/p>\u003C/div>\u003C/div>\u003Cdiv class=\"faq-item\">\u003Cdiv class=\"faq-q\">Does running at the edge guarantee multi-region checking?\u003C/div>\u003Cdiv class=\"faq-a\">\u003Cp>No, and claiming it does is how monitoring tools lose trust. Cloudflare decides where a cron invocation runs, so true multi-vantage checking means deliberately invoking from chosen locations and reconciling their verdicts, which is a design you build on top of this, not a free side effect.\u003C/p>\u003C/div>\u003C/div>\u003C/div>","\n## TL;DR\n\nI run uptime checks as Cloudflare Workers on a cron, not a daemon in one region, because one probe only reveals its own path, not your users'. The edge kills the babysat box, but true multi-vantage checking is deliberate design on top.\n\nI run my uptime monitoring platform as Cloudflare Workers on a cron trigger, not as a daemon on a box in us-east-1. The reason is simple: one server in one region does not tell you whether your site is down, it tells you whether that server's path to your site is down. Those are different questions, and conflating them produces the two failures every monitoring tool is judged on: false alarms at 2am and missed real outages. Moving the checker to the edge changes the economics and the correctness at the same time. Here is the actual design.\n\n## The problem with one probe\n\nA monitor that runs from a single region has a blind spot the size of that region. If your one probe sits in us-east-1 and us-east-1 has a bad afternoon, every site you watch goes \"down\" at once, and you page a hundred customers about an outage that is yours, not theirs. The inverse is worse: a routing problem between your probe and a target makes a perfectly healthy site look dead, and now the customer stops trusting your alerts, which is the only thing a monitoring product sells.\n\nYou also inherit an always-on server to babysit. It needs patching, it has a fixed cost whether it checks ten monitors or ten thousand, and when it dies your monitoring dies silently, which is the one component that must never fail quietly.\n\n## Why Workers, concretely\n\nCloudflare Workers on a cron trigger removes the box. My checker is a scheduled Worker that fires every minute:\n\n```toml\n# wrangler.toml\n[triggers]\ncrons = [\"* * * * *\", \"5 0 * * *\"]\n```\n\nThe first schedule runs the checks every minute. The second (`5 0 * * *`, just after midnight) runs daily rollups and cleanup, and only on one shard so the heavy job runs once, not once per shard. There is no server to keep alive. Cloudflare invokes the Worker; if a given invocation fails, the next one is sixty seconds away. The thing that watches everyone else does not have a single always-on point of failure that I have to monitor with yet another tool.\n\nThe cost model flips too. An always-on VPS bills for time it sits idle. Workers bill per invocation and per unit of work, so a minute where nothing is due to be checked costs almost nothing. That is the right shape for a workload that is bursty by nature: most monitors are not due most minutes.\n\n## Sharding, because a minute is a hard deadline\n\nHere is a constraint people miss. If checks run every minute, every batch has to finish inside a minute, including retries and database writes. As the number of monitors grows, one Worker invocation cannot fan out to all of them and still land in time. So the checker is sharded, and the shard math lives in the query that selects work:\n\n```js\nconst shard = parseInt(env.SHARD || '0')\nconst totalShards = parseInt(env.TOTAL_SHARDS || '1')\n\n// each shard owns a disjoint slice of monitors by id\n// ... WHERE m.id % ? = ?  bound to (totalShards, shard)\n```\n\nEach deployed Worker owns `m.id % totalShards == shard`, a disjoint slice of monitors, so two shards never check the same monitor and together they cover all of them. Adding capacity is deploying another shard and bumping `TOTAL_SHARDS`, not resizing a box. The slice is deterministic, so a monitor always lands on the same shard, which keeps its history coherent.\n\n## Fan-out with a hard timeout on every check\n\nInside a shard, checks run concurrently, and each one has its own abort timeout so a single slow target cannot eat the whole minute:\n\n```js\nconst controller = new AbortController()\nconst timer = setTimeout(() => controller.abort(), monitor.timeout * 1000)\ntry {\n  const response = await fetch(monitor.url, { signal: controller.signal, redirect: 'follow' })\n  clearTimeout(timer)\n  // status compared against monitor.expected_status\n} catch (err) {\n  clearTimeout(timer)\n  // AbortError becomes 'Request timeout', everything else keeps its message\n}\n```\n\nThe whole batch is dispatched with `Promise.allSettled`, not `Promise.all`, on purpose. One monitor throwing must not sink the results for every other monitor in the shard. `allSettled` gives me every outcome, good and bad, and I record them all. `all` would reject on the first failure and throw away the work already done, which in a monitoring system means losing the very signal you exist to capture.\n\n## Not every blip is an outage: confirm before you page\n\nThe edge buys location, but correctness still needs discipline in the logic, and this is where most homegrown monitors alarm too eagerly. A single failed request is not an incident. Networks hiccup. So there are two gates before anyone gets paged.\n\nFirst, an immediate retry when a check comes back down:\n\n```js\nlet result = await doFetch(monitor, headers)\nif (result.status === 'down') {\n  await new Promise(r => setTimeout(r, 2000)) // wait 2s, then look again\n  const retry = await doFetch(monitor, headers)\n  if (retry.status === 'up') result = retry\n}\n```\n\nSecond, an incident only opens when the current check is down and the previous stored check was also down. One down check is noise; two in a row is a signal. A recovery (an `up` check while an incident is open) resolves it and notifies once. That two-strikes rule, plus the inline retry, is what keeps the platform from crying wolf on a single dropped packet. It is the same instinct that keeps [payment retries from stampeding a struggling gateway](/blog/six-payments-edge-cases-that-bite-at-2am): confirm, back off, and do not react to a single blip.\n\nA monitor spends its credibility every time it pages on nothing, and it never fully earns that trust back. So I would rather lose the first few seconds of a real outage to a confirmation step than wake someone for a single dropped packet, because an alert nobody believes is worse than no alert at all.\n\n## The database has to be near the checker too\n\nIf the checker is at the edge but the database is a single Postgres box in one region, you have just moved the single point of failure and added latency to every write. Each check writes a row, and there are a lot of checks per minute. So the store is Cloudflare D1 (SQLite at the edge), bound straight into the Worker. Writes are batched in chunks so a shard with many results does not fire hundreds of individual statements inside its one-minute window:\n\n```js\nfor (let i = 0; i \u003C batch.length; i += 100) {\n  await db.batch(batch.slice(i, i + 100))\n}\n```\n\nKeeping compute and storage in the same platform is not a purity thing. It is what makes the per-minute deadline achievable at all.\n\n## What the edge does not magically give you\n\nI will be honest about the limit, because overclaiming is how monitoring tools lose trust. Running as a Worker means checks originate from Cloudflare's network rather than a fixed VPS, and it removes the babysat server. It does not, by itself, guarantee that every check leaves from a different continent every minute; Cloudflare decides where a cron invocation runs. True multi-vantage checking is a deliberate design on top of this (invoking from chosen locations and reconciling their verdicts), not a free side effect of the word \"edge.\" What the architecture does guarantee is that I am not betting my customers' trust on the health of one region and one always-on machine.\n\nThat is the trade I wanted. No server to keep breathing, a per-minute deadline I can actually hit by sharding, a store that lives next to the compute, and alerting logic that confirms before it wakes anyone. A monitoring tool earns its keep on the night it stays quiet because it correctly decided a single dropped packet was not the end of the world. That quiet is what [boring, durable code buys you](/blog/boring-code-is-a-feature-you-ship-to-your-future-self): no drama, no heroics, just a system that keeps deciding correctly while you sleep.\n\n## The takeaway\n\nMoving the checker to the edge is not about the word edge, it is about removing the two things that make a single-region monitor lie: a blind spot the size of one region and an always-on box that fails silently. Workers give me a per-invocation cost model, sharding that hits the one-minute deadline, storage in D1 next to the compute, and a confirm-before-you-page rule that keeps the alerts trustworthy. I still owe the deliberate work of true multi-vantage checking, but I am no longer betting my customers' trust on the health of one region and one machine.\n\n## FAQ\n\n**Why not just run the monitor from one VPS in us-east-1?**\nBecause a probe in one region only tells you whether that region's path to your site is healthy, so a bad afternoon in us-east-1 pages every customer at once, and a routing problem makes a healthy site look dead. Both failures destroy the trust that is the only thing a monitoring product sells.\n\n**Why Cloudflare Workers on a cron trigger instead of a daemon?**\nWorkers remove the always-on server you have to patch and babysit, and they bill per invocation instead of for idle time, which fits a bursty workload where most monitors are not due most minutes. There is no single always-on point of failure watching everyone else.\n\n**Why shard the checker?**\nBecause checks run every minute and each batch, including retries and writes, must finish inside that minute. One invocation cannot fan out to every monitor as the count grows, so each shard owns a disjoint slice by monitor id, and adding capacity is deploying another shard, not resizing a box.\n\n**Why Promise.allSettled instead of Promise.all for the fan-out?**\nBecause one monitor throwing must not sink the results for every other monitor in the shard. allSettled records every outcome, good and bad, while all rejects on the first failure and throws away work already done, which in monitoring means losing the signal you exist to capture.\n\n**How do you avoid paging on a single dropped packet?**\nTwo gates: an inline retry two seconds after a down result, and an incident that only opens when the current check and the previous stored check were both down. One down check is noise; two in a row is a signal, and a recovery resolves and notifies once.\n\n**Does running at the edge guarantee multi-region checking?**\nNo, and claiming it does is how monitoring tools lose trust. Cloudflare decides where a cron invocation runs, so true multi-vantage checking means deliberately invoking from chosen locations and reconciling their verdicts, which is a design you build on top of this, not a free side effect.\n",[22,25,28,31,34,37],{"q":23,"a":24},"Why not just run the monitor from one VPS in us-east-1?","Because a probe in one region only tells you whether that region's path to your site is healthy, so a bad afternoon in us-east-1 pages every customer at once, and a routing problem makes a healthy site look dead. Both failures destroy the trust that is the only thing a monitoring product sells.",{"q":26,"a":27},"Why Cloudflare Workers on a cron trigger instead of a daemon?","Workers remove the always-on server you have to patch and babysit, and they bill per invocation instead of for idle time, which fits a bursty workload where most monitors are not due most minutes. There is no single always-on point of failure watching everyone else.",{"q":29,"a":30},"Why shard the checker?","Because checks run every minute and each batch, including retries and writes, must finish inside that minute. One invocation cannot fan out to every monitor as the count grows, so each shard owns a disjoint slice by monitor id, and adding capacity is deploying another shard, not resizing a box.",{"q":32,"a":33},"Why Promise.allSettled instead of Promise.all for the fan-out?","Because one monitor throwing must not sink the results for every other monitor in the shard. allSettled records every outcome, good and bad, while all rejects on the first failure and throws away work already done, which in monitoring means losing the signal you exist to capture.",{"q":35,"a":36},"How do you avoid paging on a single dropped packet?","Two gates: an inline retry two seconds after a down result, and an incident that only opens when the current check and the previous stored check were both down. One down check is noise; two in a row is a signal, and a recovery resolves and notifies once.",{"q":38,"a":39},"Does running at the edge guarantee multi-region checking?","No, and claiming it does is how monitoring tools lose trust. Cloudflare decides where a cron invocation runs, so true multi-vantage checking means deliberately invoking from chosen locations and reconciling their verdicts, which is a design you build on top of this, not a free side effect.","10 min read",10,1785070532920]