Skip to content
Revenue-critical code cover
writing

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.

Hasan MisbahHasan Misbah10 min read
uptime-monitoring
cloudflare-workers
edge
architecture
reliability
TL;DR

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.

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.

The problem with one probe

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.

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.

Why Workers, concretely

Cloudflare Workers on a cron trigger removes the box. My checker is a scheduled Worker that fires every minute:

# wrangler.toml
[triggers]
crons = ["* * * * *", "5 0 * * *"]

The 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.

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.

Sharding, because a minute is a hard deadline

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:

const shard = parseInt(env.SHARD || '0')
const totalShards = parseInt(env.TOTAL_SHARDS || '1')

// each shard owns a disjoint slice of monitors by id
// ... WHERE m.id % ? = ?  bound to (totalShards, shard)

Each 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.

Fan-out with a hard timeout on every check

Inside a shard, checks run concurrently, and each one has its own abort timeout so a single slow target cannot eat the whole minute:

const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), monitor.timeout * 1000)
try {
  const response = await fetch(monitor.url, { signal: controller.signal, redirect: 'follow' })
  clearTimeout(timer)
  // status compared against monitor.expected_status
} catch (err) {
  clearTimeout(timer)
  // AbortError becomes 'Request timeout', everything else keeps its message
}

The 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.

Not every blip is an outage: confirm before you page

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.

First, an immediate retry when a check comes back down:

let result = await doFetch(monitor, headers)
if (result.status === 'down') {
  await new Promise(r => setTimeout(r, 2000)) // wait 2s, then look again
  const retry = await doFetch(monitor, headers)
  if (retry.status === 'up') result = retry
}

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 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: confirm, back off, and do not react to a single blip.

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.

The database has to be near the checker too

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:

for (let i = 0; i < batch.length; i += 100) {
  await db.batch(batch.slice(i, i + 100))
}

Keeping compute and storage in the same platform is not a purity thing. It is what makes the per-minute deadline achievable at all.

What the edge does not magically give you

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.

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 boring, durable code buys you: no drama, no heroics, just a system that keeps deciding correctly while you sleep.

The takeaway

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.

FAQ

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.

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.

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.

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.

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.

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.

let's talk

Building something where this kind of work matters?

I'm in GMT+6 and work async-first, so your timezone is never a reason not to reach out.

Get in touch
Copyright © Hasan Misbah. All rights reserved.
Hasan Misbah