Engineering·8 min read

A Refusal Is a Response: Why a 403 Is Not Downtime

We publish a public board that measures other people's websites, and for a while it told readers something untrue: a domain whose edge network declined our probe was rendered exactly like a domain that was failing. Here is the bug, the fix, and the numbers from the window we found it in — 22,819 probes across 7,104 domains, why ‘not measured’ must never share a dash with zero, and the cadence arithmetic we expect you to check.

E

Akshay Dubey

Founder

We publish a public board that measures other people's websites, and for a while it told readers something that was not true. A domain whose edge network declined our request was rendered exactly like a domain that was failing. Same word, same colour, same row. The site was fine. Our board said otherwise, about a company we had named.

This is what we found, what we changed, and the numbers from the window we found it in.

Every probe we issued in 30 days, and what came backRadar, 720-hour window, measured 2026-09-14T06:50Z. Active measurement only: our own requests to public endpoints.22,819probes issued22,659answered (99.30%)160 with no response at all (0.70%)22,659answered, by status family2xx4xx3xx = 1 probe in 30 days: a hairline at this scale, so we draw the tick, not a bar2xx19,9734xx2,6035xx823xx1The four families sum to 22,659 exactly: zero unclassified remainder, nothing rounded into a bucket.A refusal is a response, so a refused probe is counted here as answered. It is not evidence that a site is down. Every figure below was measured on 2026-09-14 at 06:50 UTC, over a 30-day window, from one observing region (us-central1). They will have moved by the time you read this — the board at echelongraph.io/radar is the live version.

What the instrument actually is

Radar makes its own HTTP requests to public endpoints and records what comes back. That is the whole method. It never intercepts, sniffs, or passively captures anyone else's traffic — active measurement only, which is the only kind we think a company should publish about hosts it does not own.

The bot says who it is. It sends EchelonGraphBot/1.0 (+https://echelongraph.io/bot) and an X-EchelonGraph-Verify header carrying a URL you can open. A user agent is a claim, not proof — anyone can type ours — so echelongraph.io/bot exists to let you check the claim against something we publish and can be held to.

A refusal is a response

Here is the bug, and it is embarrassing in the way that useful bugs are.

Of 22,819 probes, 22,659 got a response and 160 got nothing at all. The responses break into four families: 19,973 were 2xx, 2,603 were 4xx, 82 were 5xx, and exactly 1 was 3xx. Those four sum to 22,659 with no remainder.

The 4xx band is where we went wrong. A 403 or a 429 is a *response*. The server was reachable, it understood us, and it declined. That is a fact about the transaction between us and an edge network — it is not evidence that the site is broken for anybody else. Our board rendered it as though it were.

Same bucket, different meaning: a refusal is not a failureIllustrative synthetic domains. Both rows returned a 4xx status. Only one of them was declined at the edge.BEFORE: one bucketevery 4xx rendered the same waydomain-a.example4xxrendered as a failing checkdomain-b.example4xxrendered as a failing checkNaive read: two failing sites.Both of them answered us.AFTER: two classes, said in prosethe board names which one it isdomain-a.example403 refused at the edgeanswered, and the answer was nodomain-b.example404 client error from the sitethe request reached the site and it repliedOn the board, a refusal is HTTP 403 or 429,counted over the 30-day window.We shipped a board that rendered these two rows identically, and we fixed it. A refused probe still counts as answered. Both rows look identical on a naive board. They are not the same event, and only one of them is about the site.

We fixed it by giving a refusal its own word and its own colour, and by stating in visible prose on the board — not in a tooltip — that a refusal is a response and therefore counts inside "answered". The figure did not change. What changed is that it stopped being painted as a health score.

Two things we still cannot tell you, and I would rather say so than imply otherwise. We do not publish how many of those 2,603 were 403 or 429 specifically. And we do not record *why* we were declined — that would require capturing and attributing response headers we deliberately do not store, so any claim about which vendor or which rule refused us would be an inference dressed as a measurement.

Three states, never two

The rule that came out of this generalises past status codes, and it is the single most useful thing in this post if you are building something similar.

"Not measured" is not the same as "measured and absent", and neither is the same as "a measured value." Collapse the first two into one dash and your dashboard will quietly publish an unmeasured state as a clean one.

Three states, never two: the certificate facet7,104 domains tracked. A dash that means we did not look must never look like a dash that means nothing is there.6,169 certificates actually readthe rest?7,104domains tracked106 expiring within 14 days1 · not measuredNo probe has read a certificatefor this domain yet.count: not publisheda dash here would be a lie2 · measured and absentWe looked. There was nocertificate to read.count: not publisheda dash here would be a lie too3 · measured valueA certificate we read, withthe expiry date it carries.6,169 domains106 of them expire within 14 daysStates 1 and 2 both sit inside the block marked ?. We do not publish that split, because separating them needs ameasurement we have not made. Subtracting one column from another would not measure it, it would invent it. Concretely: we read a TLS certificate for 6,169 of the 7,104 domains we tracked, and 106 of those expire within 14 days. Notice what we are not doing — we are not subtracting to tell you about the rest. That remainder mixes "we have not got to it yet" with "we looked and there was nothing to read", and those are different facts about different hosts. A single number there would be a worse answer than no number.

Latency, and the shape of the tail

The median is not the experience: time to first byteRadar, 30-day window, measured 2026-09-14T06:50Z. Three summaries of one measurement, on a single scale.p50 to p95: the spread a single average hidesp50 (median)354 msmean535 msp951,503 ms0500 ms1,000 ms1,500 msHalf of all probes answered faster than 354 ms. The slowest five per cent came back beyond 1,503 ms.The mean, 535 ms, sits above the median. That gap is the tail, and an average on its own would hide it. Median time-to-first-byte was 354 ms; the mean was 535 ms; the 95th percentile was 1,503 ms. The mean sitting well right of the median is the tail doing its work, and it is why a median alone is a poor basis for an alert threshold. We are not going to tell you whether 1,503 ms is good — that depends on what the host is and who it serves, and we did not measure it against any target.

One more that we deliberately report without a denominator: for 13,258 probes the final response came from a different host than the one we asked for. We follow redirect chains and record the final hop, which is also why 3xx appears exactly once — a redirect is usually not where the chain ends. We are not converting that count into a percentage, because the honest denominator is arguable and the number is interesting enough without one.

The arithmetic you are about to do

A careful reader divides. 22,819 probes across 7,104 domains is 3.2 probes per domain over a thirty-day window — roughly one every nine days. We publish a 24-hour interval. Those do not obviously agree, so here is the reconciliation.

The 24-hour figure is a floor on how often we will contact one host we do not own — a politeness bound, a minimum spacing between our requests — and never a promise of daily coverage. Underneath it, only 8 of the window's 30 days carry any data at all: the first bucket is 2026-09-07. The registry is still being walked for the first time, and the sweep is bursty rather than smooth — three large passes of 7,546, 6,433 and 6,384 probes, separated by days of 180, 651 and 840.

So the average is genuinely below the published interval, and that is the correct direction for a bound like this one to be wrong in. The fuller accounting, including how many hosts we contact in a day and what the prober refuses before it makes a request, is on the bot page.

If it is your domain, you can ask for more

Everything above is the stranger's cadence. A domain owner can prove control with a DNS TXT record and turn on live monitoring at a much shorter interval.

Consent first, and a loop that stops by itselfLive monitoring runs only for a domain whose owner proved they own it, and only while a screen is open.1 · Ownership proofThe domain owner publishesthe DNS TXT record we name.2 · VerifiedWe read that record from DNSbefore any check is scheduled.3 · Checks beginRe-check interval floor:30 seconds. Never faster.The watch leaseAn open screen renews it every 30 s. It expires 120 s after the last renewal.screen closescheckrenewrenewrenewchecks every 30 s while the screen is open120 s lease, and nothing renews itlease lapseschecks stop3domains one screen may watch25distinct domains under live monitoring (cap)72,000probes a day, published ceilingThat ceiling is 25 domains x 24 h at the 30-second floor. Hosts we do not own are re-probed on a published 24-hour interval.The 30-second floor exists only where the domain owner asked us to watch. Ownership is proven by publishing a record we give you. Once proven, checks run at a 30-second floor — but only while a screen is open and holding a lease, which is renewed every 30 seconds and expires 120 seconds after the last renewal. Close the tab and the checks stop on their own. One screen may watch 3 domains; 25 distinct domains may be under live monitoring at once; and the published worst case is 72,000 probes a day, which is a ceiling computed from the floor and the cap, not a description of current activity.

Consent shortens the interval. It does not remove the bound — that is the difference between a host that asked for attention and one that did not. You can start at echelongraph.io/radar/monitor.

What this post does not contain

No host names, no customer data, no per-domain findings about anyone. The per-domain pages on our own board are deliberately excluded from our sitemap and marked noindex, because letting a search engine enumerate a measured security posture for a named third party — under our domain's authority — is a different act from showing it to someone who went looking for that one domain.

The instrument was wrong, in public, about companies we named. We would rather publish that than the version where the dashboard was always right.

Protect your infrastructure before the breach

Map your attack surface, automate compliance, and detect insider threats in real time.

Start free trial →