16 Keyless DNS and Cert APIs. Only 3 of 12 Answer for Themselves.
Before any of the API work, I asked my own machine to resolve three domains. July 20, 2026, same minute:
example.com local=198.18.0.131
github.com local=198.18.0.30
iana.org local=198.18.1.244
Three different domains, three addresses in the same tiny block. I looked up what that block is, using one of the keyless endpoints this post is about:
name= SPECIAL-IPV4-BENCHMARK-TESTING-IANA-RESERVED
remarks= Addresses starting with "198.18." or "198.19." are set aside for use in
isolated laboratory networks used for benchmarking and performance testing.
They should never appear on the Internet.
Then I asked it for a hostname that does not exist. It answered that too:
zzz-no-such-host-991.example 198.18.1.247
zzz-no-such-host-992.example 198.18.2.7
zzz-no-such-host-993.example 198.18.2.8
another-fake-name-4412.test 198.18.2.9
Consecutive addresses, handed out in order, for names nobody has ever registered. So this is not a resolver returning the wrong site. It is a resolver minting an address on demand and handing it back with the same confidence it uses for real ones. The one name that did fail, qqq-definitely-not-real-7788.invalid, threw gaierror, which at least tells me the failure path still exists.
My resolver was not telling me about example.com. It was telling me about the proxy sitting between me and example.com. Every lookup I ran locally would have described that proxy, confidently and with a valid answer.
That turned out to be the theme of the whole day.
The one idea in this post
A free DNS or certificate API answers questions about domains, certificates and IP ownership with no API key, no signup, no card. Sixteen clear that bar and I verified each with a live curl on July 20, 2026. The table is one screen down.
Under it is the part I care about. When you ask “who is behind this domain”, these endpoints answer about whoever holds the address block you landed on, which is usually not the site and is not always a CDN either. Sometimes it is a proxy on your own machine. And in three cases out of twelve, the field that looks like the answer was a label that names nobody you would recognise.
I got that last part wrong on my first pass, and then I got the reason for it wrong too. Both versions are below, including the wrong output.
The 16 endpoints, verified
Every row below is a live check at 19:44Z on July 20, 2026. I ran negative controls first, so I know the probe can report failure: this-host-does-not-exist-zzz9.example returned 000, dns.google/zzz-no-such-endpoint-991 returned 404, rdap.org/domain/zzz-no-such-tld-991.invalidtld returned 404, internetdb.shodan.io/999.999.999.999 returned 404, and RIPEstat without its required parameter returned 400.
Byte counts are what came back on that pass and they drift as the underlying data changes. Cert Spotter went from 2,344 bytes at 19:04Z to 2,892 at 19:44Z because a new certificate landed in between. Read the column as evidence of a non-empty body, not as a constant.
| # | Endpoint | What it answers | HTTP | bytes |
|---|---|---|---|---|
| 1 | dns.google/resolve?name=&type= | DNS over HTTPS, JSON | 200 | 296 |
| 2 | cloudflare-dns.com/dns-query | DoH, JSON (needs accept: application/dns-json) | 200 | 251 |
| 3 | dns.nextdns.io/dns-query | DoH, JSON | 200 | 370 |
| 4 | crt.sh/?q=&output=json | certificate transparency search | 200 / 502 | 26229 when up |
| 5 | api.certspotter.com/v1/issuances | CT issuances per domain | 200 | 2892 |
| 6 | ct.googleapis.com/logs/us1/argon2026h2/ct/v1/get-sth | signed tree head of a CT log | 200 | 245 |
| 7 | rdap.org/domain/<domain> | registration data for a domain | 200 | 2355 |
| 8 | rdap.org/ip/<ip> | who holds an IP block | 200 | 7134 |
| 9 | rdap.arin.net/registry/ip/<ip> | same, straight from ARIN | 200 | 7134 |
| 10 | rdap.db.ripe.net/ip/<ip> | same, straight from RIPE | 200 | 10445 |
| 11 | data.iana.org/rdap/dns.json | which RDAP server serves which TLD | 200 | 70987 |
| 12 | internetdb.shodan.io/<ip> | open ports, CPEs, hostnames, tags | 200 | 147 |
| 13 | stat.ripe.net/data/network-info/data.json?resource=<ip> | prefix and ASN for an IP | 200 | 406 |
| 14 | stat.ripe.net/data/as-overview/data.json?resource=<asn> | ASN holder name | 200 | 656 |
| 15 | api.hackertarget.com/dnslookup/?q= | plain-text DNS records | 200 | 312 |
| 16 | api.hackertarget.com/aslookup/?q= | ASN + prefix, one line | 200 | 53 |
Two notes on that table, both of which cost me time.
The RIPEstat rows need ?resource=. I originally wrote them without it, and the bare URL is not a live check at all: stat.ripe.net/data/network-info/data.json returns 400 with a 415-byte error body. Only ?resource=151.101.2.132 gives the 200 and the 406 bytes above. I had the wrong URL sitting next to the right status code, which is exactly the kind of row that makes a table look verified when it is not.
Row 6 changed shards on me. I first measured argon2026h1 and it answers 200 happily. But it is no longer in Chrome’s CT log list: fetching gstatic.com/ct/log_list/v3/log_list.json at 19:46Z returns two Argon logs, argon2026h2 (usable, covering 2026-07-01 to 2027-01-01) and argon2027h1. The h1 shard covers certificates that have already expired, so it still serves, it just is not somewhere new certificates go. Recommending it would have been recommending a museum.
I also verified api.ssllabs.com/api/v3/info (200, 285 bytes), which is keyless but is an info endpoint rather than a lookup, so I left it off the numbered list.
Three that did not make it, and why:
dns.quad9.net:5053/dns-queryreturned000from here, and on the standard port answered400with the bodyDoH unable to decode BASE64-URL, which is the honest reply: that path speaks RFC 8484 wire format, not the JSON parameter style Google and Cloudflare accept. I want to be careful about the port, though.dns.quad9.netresolves on this machine to198.18.1.235, which is the interceptor from the top of this post. So “port 5053 did not open” is a fact about my network path, not a fact about Quad9. Same hedge as the next one.api.bgpview.io/ip/<ip>returned000three times in a row. I could not reach it at all today. Whether it is down globally or blocked from my network, I did not establish.crt.shis on the list but it flapped while I was working, and it was still flapping hours later. At 19:45Z I hit it four times in twelve seconds:502,000,200,200. The two successes were 26,229 bytes each, byte-identical to the ones I got at 18:50Z. It is genuinely useful and it is genuinely unreliable. Treat a single 502 from it as noise, not as an answer.
That last pattern showed up on ARIN too. rdap.arin.net gave me 000 during the 19:44Z sweep, then 200 with 7,134 bytes on four straight retries a minute later. One failed request from any of these endpoints means nothing. I nearly wrote ARIN off on a single sample.
What happens when you ask who is behind a domain
The natural pipeline with these endpoints is two hops. Resolve the domain to an A record with a DoH endpoint. Look up that IP with RDAP. Read the owner.
I ran it against 12 well-known domains where I could state in advance who the site’s own operator is. Here is a full run, 19:40Z, July 20, 2026:
example.com 104.20.23.154 A=2 CLOUDFLARENET THIRD-PARTY-NETWORK Cloudflare, Inc.
iana.org 192.0.43.8 A=1 ICANN OWN-NETWORK ICANN
mit.edu 23.197.133.160 A=1 AKAMAI THIRD-PARTY-NETWORK Akamai Technologies, Inc.
ietf.org 104.16.44.99 A=2 CLOUDFLARENET THIRD-PARTY-NETWORK Cloudflare, Inc.
nasa.gov 192.0.66.108 A=1 AUTOMATTIC THIRD-PARTY-NETWORK Automattic, Inc
kernel.org 172.105.4.254 A=1 LINODE THIRD-PARTY-NETWORK Linode
fsf.org 209.51.188.174 A=1 HURRICANE-5 THIRD-PARTY-NETWORK Hurricane Electric LLC
python.org 151.101.0.223 A=4 SKYCA-3 THIRD-PARTY-NETWORK Fastly, Inc.
wikipedia.org 185.15.58.224 A=1 US-WMF-20130110 OWN-NETWORK Wikimedia Foundation, Inc.
github.com 140.82.121.3 A=1 GITHU OWN-NETWORK GitHub, Inc.
apache.org 151.101.2.132 A=1 SKYCA-3 THIRD-PARTY-NETWORK Fastly, Inc.
debian.org 151.101.130.132 A=4 SKYCA-3 THIRD-PARTY-NETWORK Fastly, Inc.
own-network=3 third-party=9 error=0 of 12
Only 3 of 12 answer for themselves. ICANN, Wikimedia and GitHub. Everyone else hands you an address that belongs to a company they pay.
The 3/9 split held across every clean run I made, at 18:54Z, 18:55Z, 18:57Z, 19:40Z and 19:41Z. One run at 18:53Z scored 3 own-network and 7 third-party because RDAP threw a transport error on mit.edu and apache.org and my code counted those two as errors rather than guessing. Same verdicts everywhere they resolved, so I am comfortable quoting the ratio, with the note that 2 of 12 lookups failed outright on one run out of six.
The 9 are not 9 CDNs
This is where my first headline was wrong, and it was wrong against my own table.
“Third-party network” is not the same claim as “CDN”. So I checked the nine the other way round, by asking what the site actually serves through. Response headers at 19:42Z:
example.com server: cloudflare cf-ray: a1e47377fc1cc4fa-WAW
mit.edu Server: AkamaiGHost
ietf.org server: cloudflare cf-ray: a1e473884fabbf7e-WAW
python.org via: 1.1 varnish x-served-by: cache-bma-essb1270026-BMA
apache.org via: 1.1 varnish x-served-by: cache-hel1410028-HEL, cache-bma...
debian.org via: 1.1 varnish x-served-by: cache-bma-essb1270023-BMA
nasa.gov server: nginx x-cache: HIT
kernel.org server: nginx
fsf.org Server: nginx Via: 1.0 www.fsf.org (squid/3.1.19) X-Cache: MISS
Six carry a third-party CDN’s fingerprint. Three do not. Shodan’s InternetDB, endpoint 12 on the list above, agrees row for row: tags:["cdn"] on the Cloudflare, Akamai and Fastly addresses, tags:[] on nasa.gov and fsf.org, and tags:["cloud"] on kernel.org’s Linode address. Two independent methods, same six.
So the honest count is 9 of 12 on somebody else’s network, of which 6 are a CDN. Automattic is a hosting relationship. Hurricane Electric is transit. Linode is a rented server. None of that is visible in the RDAP response, which returns the identical shape of JSON for all three.
What the verdict actually measures
Renaming IN-FRONT to THIRD-PARTY-NETWORK was not cosmetic. The old name asserted something my method cannot see.
RDAP answers “who holds this address block”. It does not answer “who sits in front of the origin”. Those two questions give different answers, and three of my own rows show the gap:
fsf.orghas no third party in front of it at all. The cache it serves through is its own:Via: 1.0 www.fsf.org (squid/3.1.19). Hurricane Electric just holds the /19. My probe called it an intermediary; there is no intermediary.kernel.orghas a real CDN, and my probe missed it. The apex returns bareserver: nginxon a Linode address. Butwww.kernel.orgreturnsvia: 1.1 varnishandx-served-by: cache-bma-essb1270055-BMA, which is Fastly. So the answer was “Linode” while the actual edge lives on a hostname I never asked about. Wrong company, and for a reason that has nothing to do with RDAP.nasa.govis a hosting relationship, not a CDN one, and nothing in the response says so.
The verdict is still worth computing. It reliably tells you “the address you are about to monitor is not owned by the people who run the site”, which is a real and useful thing to know. It just does not tell you what that third party is doing, and the label should not pretend otherwise.
The field that looked like the answer
Here is the mistake, because it is the useful part. There are two layers to it, and I only found the second one after someone pushed back on the first.
My first version read RDAP’s top-level name field. It gave me this:
python.org 151.101.0.223 INTERMEDIARY SKYCA-3
wikipedia.org 185.15.58.224 INTERMEDIARY US-WMF-20130110
github.com 140.82.121.3 INTERMEDIARY GITHU
Three wrong calls in one screen. US-WMF-20130110 is Wikimedia’s own block and GITHU is GitHub’s own block, so both should have been OWN-NETWORK. My check compared against the strings wikimedia and github and missed.
Then I explained it backwards. I wrote that name is the registry’s handle for the network, and that GITHU was five characters of ARIN bookkeeping. That is wrong, and the spec says so in one line. Scott Hollenbeck of Verisign Labs and Andy Newton of AWS, who wrote RFC 9083 (June 2021, the standard that defines these JSON responses), list the members of the IP network object in section 5.4:
* handle -- a string representing the RIR-unique identifier of the
network registration
* name -- a string representing an identifier assigned to the
network registration by the registration holder
It is the other way round from what I said. handle is the registry’s identifier. name is a label chosen by the holder. Nobody at ARIN typed GITHU. GitHub did.
Which makes the practical lesson stronger, not weaker. If name were registry bookkeeping, you could at least expect it to follow a registry’s naming discipline. It is not. It is a free-text label its owner picked and has no obligation to keep meaningful.
Counting my own twelve rows: seven spell out the operator (CLOUDFLARENET twice, ICANN, AKAMAI, AUTOMATTIC, LINODE, HURRICANE-5). Five do not: SKYCA-3 three times, US-WMF-20130110, and GITHU, which is a truncation that reads like the company to a human and fails a substring test, since github is not a substring of githu. That is the worst possible hit rate. High enough to look like it works, low enough that my first check got three verdicts wrong. The other two misses happened to land on the right answer for the wrong reason: SKYCA-3 on apache.org and debian.org really is a third-party network, so a broken test still printed a correct verdict twice.
SKYCA-3 is the one that reads like line noise. It is the holder’s label on a block registered to Fastly, ARIN registration date 2016-02-01, last changed 2021-12-14. Pull the full object and the organization is right there, one level down:
name : SKYCA-3
handle : NET-151-101-0-0-1
startAddr : 151.101.0.0 - 151.101.255.255
entity: SKYCA-3 ['registrant'] kind=org fn= ['Fastly, Inc.']
Reading the right field, properly this time
The operator name lives in entities[], in an entry whose roles contains registrant, inside a vCard array. My first fix walked that list and returned the first registrant it found.
That fix was also wrong, and the row that proves it is one I had already printed. RIPE’s answer for 185.15.58.224, Wikipedia’s address, carries three registrant entities:
registrant handle=ORG-WFI2-RIPE kind=org fn=['Wikimedia Foundation, Inc.']
registrant handle=RIPE-NCC-HM-MNT kind=individual fn=['RIPE-NCC-HM-MNT']
registrant handle=WIKIMEDIA-MNT kind=individual fn=['WIKIMEDIA-MNT']
Two of those are maintainer objects. Array order is not guaranteed by anything, so “take the first registrant” is a coin flip that happened to land right for me. On a different ordering my code would have reported RIPE-NCC-HM-MNT as the operator of Wikipedia, and printed it with total confidence.
kind is what separates them. Take every registrant, prefer the ones that are organizations, and refuse to answer when that leaves more than one candidate:
def _vcard(ent, key):
for f in (ent.get("vcardArray") or [None, []])[1] or []:
if f[0] == key:
return f[3]
return None
def ip_org(ip):
"""RDAP can carry SEVERAL registrant entities: the real holder plus
maintainer objects. Array order is not guaranteed, so take all of
them and prefer kind=org."""
st, d = get_json("https://rdap.org/ip/%s" % ip)
if st != 200:
return None, None, "rdap HTTP %s" % st
net_name = d.get("name")
regs = [e for e in d.get("entities", [])
if "registrant" in (e.get("roles") or [])]
if not regs:
return net_name, None, "no registrant entity"
orgs = [e for e in regs if _vcard(e, "kind") == "org" and _vcard(e, "fn")]
if orgs:
names = sorted({_vcard(e, "fn") for e in orgs})
if len(names) == 1:
return net_name, names[0], None
return net_name, None, "ambiguous: %d org registrants %s" % (
len(names), names)
fns = sorted({_vcard(e, "fn") for e in regs if _vcard(e, "fn")})
if len(fns) == 1:
return net_name, fns[0], "no kind=org; single fn used"
return net_name, None, "no kind=org, %d candidate fns %s" % (len(fns), fns)
Same two endpoints, same 200s, same JSON. The difference between a wrong answer and a right one was which key I read, twice over. Nothing in the response flagged either wrong read, because neither was malformed. They were just different questions’ answers.
This is the same shape as the 198.18 thing at the top. Valid response, plausible content, wrong subject.
The addresses that did not move
I originally wrote that the IPs were shifting between runs and that you had cached a coin flip. The conclusion was right. The explanation was an artifact of my own code, and it is worth showing because it is such a common one.
One DoH response carries the whole A record set. At 19:38Z:
python.org A_count=4 151.101.192.223,151.101.0.223,151.101.128.223,151.101.64.223
debian.org A_count=4 151.101.66.132,151.101.2.132,151.101.194.132,151.101.130.132
example.com A_count=2 172.66.147.243,104.20.23.154
ietf.org A_count=2 104.16.45.99,104.16.44.99
mit.edu A_count=1 23.197.133.160
My code read Answer[0]. dns.google rotates the order between responses, so index 0 kept changing and I reported it as the address moving. It was not. I queried each of those names twice in a row and compared the sets:
python.org set_equal=True ['151.101.0.223','151.101.128.223','151.101.192.223','151.101.64.223']
debian.org set_equal=True ['151.101.130.132','151.101.194.132','151.101.2.132','151.101.66.132']
example.com set_equal=True ['104.20.23.154','172.66.147.243']
Identical sets, rotated order. Nothing moved. I turned a load-balancing convention into a claim about the network, which is the sort of thing I complain about when other people do it.
Two rows are genuinely unstable, and I want to keep them separate from the artifact. github.com has A_count=1 and still returned 140.82.121.3 on one run and 140.82.121.4 on another. mit.edu also has A_count=1 and changed organization between runs, from Akamai Technologies, Inc. to Akamai International, BV, because a different edge address landed in a different registry object. Those two are real variation over time. The four multi-address names were never variation at all.
The fix is one line and it changes what the probe can claim: take the full RRset, sort it, pick deterministically. Then “the address changed” means the address changed.
If you cache one address out of four and treat it as “the server”, you have cached one member of a rotating set. The verdict is stable. The address is a sample.
Making the check able to fail
I write a lot of these little probes and the failure mode I keep hitting is a check that can only produce one verdict. It prints green, I believe it, and it was never capable of printing anything else.
So the probe ends like this:
own = [r for r in rows if r["verdict"] == "OWN-NETWORK"]
third = [r for r in rows if r["verdict"] == "THIRD-PARTY-NETWORK"]
if not own:
print("PROBE UNUSABLE: never returned OWN-NETWORK, so it cannot tell "
"'someone else's network' from 'answers for itself'.")
return 2
if not third:
print("PROBE UNUSABLE: never returned THIRD-PARTY-NETWORK on this sample.")
return 2
return 0
Both branches, run today, so you can see them rather than take my word for it.
Mixed sample, 19:40Z:
own-network=3 third-party=9 error=0 of 12
exit=0
Same code, two minutes later, sample narrowed to four domains that are all behind a CDN, 19:42Z:
example.com 104.20.23.154 A=2 CLOUDFLARENET THIRD-PARTY-NETWORK Cloudflare, Inc.
ietf.org 104.16.44.99 A=2 CLOUDFLARENET THIRD-PARTY-NETWORK Cloudflare, Inc.
python.org 151.101.0.223 A=4 SKYCA-3 THIRD-PARTY-NETWORK Fastly, Inc.
debian.org 151.101.130.132 A=4 SKYCA-3 THIRD-PARTY-NETWORK Fastly, Inc.
own-network=0 third-party=4 error=0 of 4
PROBE UNUSABLE: never returned OWN-NETWORK, so it cannot tell 'someone else's
network' from 'answers for itself'.
exit=2
A probe that answers “everything is on someone else’s network” on every input is not measuring anything. iana.org is the row that keeps mine honest: it resolves to 192.0.43.8 and RDAP says ICANN, which is IANA’s own organization. If that row ever starts reporting THIRD-PARTY-NETWORK, either the world changed or my check broke, and I want to know which.
The certificate says the same thing
Certificate transparency tells the story from the other side. Cert Spotter, at 19:03Z, for example.com:
not_before 2025-12-02T00:00:00Z | issuer: C=GB, O=Sectigo Limited, CN=Sectigo Public Server Authentication CA OV R36
not_before 2026-05-31T21:39:00Z | issuer: C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing RSA CA 3
not_before 2026-05-31T21:39:12Z | issuer: C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
example.com is IANA’s demonstration domain, about as canonical as a hostname gets, and its two newest certificates come from a Cloudflare issuing CA. Note the organization is SSL Corporation while the common name says Cloudflare, so even inside one issuer string there are two different companies to read. crt.sh agreed when it was up: 69 records, first issuer C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing RSA CA 3, reproduced at 26,229 bytes on every successful fetch across an hour.
I also got the CT log numbers backwards on my first pass, and the fix is the same shape as everything else in this post: I measured a dead thing and generalized from it.
I had written that a log’s tree_size only moves when the log re-signs its tree head. What I actually measured was argon2026h1, the retired shard, which reported tree_size=2807500498 at 19:02Z and 2807500502 at 19:46Z. Four entries in forty-four minutes. Here is the live shard, argon2026h2, read three times about thirteen seconds apart:
19:47:32Z tree_size=2082146064
19:47:46Z tree_size=2082148174
19:47:59Z tree_size=2082150799
That is roughly two thousand new entries every thirteen seconds. The number moves because certificates are being issued, not because the log decided to re-sign. Causality pointed the wrong way, and I only saw it because I switched shards for an unrelated reason.
About the machine all of this was measured on
Everything above was collected from behind that 198.18 interceptor. For a post whose hook is “your resolver is lying to you”, I should say plainly why I still trust the numbers.
The interception is at the DNS layer only. TLS goes through untouched, and the certificates I get are the real ones from the real issuers:
dns.google issuer=C=US, O=Google Trust Services, CN=WE2 subject=CN=dns.google
rdap.org issuer=C=US, O=Google Trust Services, CN=WE1 subject=CN=rdap.org
crt.sh issuer=C=GB, O=Sectigo Limited, CN=Sectigo ... R36 subject=CN=crt.sh
No local CA anywhere in those chains, and curl reports ssl_verify=0 on all of them. That check can fail, too: expired.badssl.com returns ssl_verify=10 and exit code 60 from the same machine, so verification is genuinely being enforced rather than skipped.
So the payloads are authentic end to end, and crt.sh coming back byte-identical at 26,229 bytes across an hour is a second, cruder confirmation. What I cannot claim from here is anything about reachability: every 000 in this post, including Quad9’s port 5053 and bgpview entirely, is a statement about my network path and not about those services.
Gotchas worth knowing before you wire these in
rdap.org redirects and you must follow it. rdap.org/domain/example.com returns 302 bare and 200 with -L. If your HTTP client does not follow redirects by default, you get an empty-looking failure that has nothing to do with the domain.
Not every TLD is in RDAP bootstrap. rdap.org/domain/mit.edu returns 404, even followed. That is .edu not having an RDAP service in the bootstrap file, not MIT being unregistered. The bootstrap file itself (endpoint 11, 70,987 bytes) is what tells you which TLDs are covered, and it is worth fetching once rather than guessing.
A 404 can be a real answer. internetdb.shodan.io returns 404 with the body {"detail":"No information available"} for an IP it has nothing on. That is not an error. It is the endpoint saying it has no data, which is different from the endpoint being broken, and if your code only checks the status code you will conflate the two.
One failed request is not a signal. ARIN gave me 000 once and 200 on four straight retries a minute later. crt.sh gave me 502, 000, 200, 200 in twelve seconds. If your monitoring alerts on a single sample from any of these, it will page you for nothing.
Your own resolver is a participant. The 198.18 addresses at the top of this post are the version of this problem that hits closest to home. Corporate DNS, a VPN, a container runtime and a sandbox will all happily answer your gethostbyname with something that has nothing to do with the domain, and as the fake hostnames showed, some of them will invent an address rather than admit the name does not exist. Using a DoH endpoint instead of the system resolver is not paranoia, it is how you find out that your system resolver had an opinion.
Why this bothers me specifically
I run scrapers in production, across 32 actors and something over 2,000 runs. The recurring argument with monitoring is exactly this one. Health check goes green, the target’s edge is up, and our extraction is returning nothing useful, because “the site is up” was measured against a CDN edge and “our data is flowing” depends on an origin nobody checked.
I want to be precise about what I did not measure here. I did not pull our own logs to tell you what share of our production targets sit behind a CDN. I could have guessed from memory and I would rather not. So: no number from me on that, and if you see one in a post like this without a method attached, ask where it came from.
What I did not check
- Rate limits. None of these 16 endpoints were tested under load. I made single requests, a few hundred of them across two hours. crt.sh’s 502s might be load-related, or might not; I have no evidence either way.
- Whether these stay keyless. They were keyless on July 20, 2026. That is all I can tell you.
- IPv6. Every lookup above is an A record. AAAA behaviour may differ and I did not test it.
- Whether Shodan’s
cdntag is right in general. It matched my header evidence on all 12 rows, which is encouraging and is not validation. I do not know how Shodan derives it. - Anything outside my vantage point. One machine, one network, one afternoon, and that network turned out to have a proxy in it. Your results will differ, which is sort of the point.
The framing throughout is defensive: understanding what your own monitoring is describing. Everything here is public registration and transparency data about domains I do not operate, queried the way the registries intend.
The open question
The bit I have not solved: I can tell that a domain answers as Cloudflare or Fastly, and Shodan’s cdn tag will even confirm it for me, but none of that tells me whether the origin behind the edge is healthy. The edge will happily serve a cached 200 over a dead origin. apache.org served me x-cache: HIT, HIT with age: 2843 while I was writing this, and I have no way to know from outside what state the thing behind that cache was in.
Every keyless signal I found describes the layer I can already reach.
If you monitor sites you do not control, how do you distinguish “the CDN is up” from “the thing behind the CDN is up”? I have a couple of bad ideas involving cache headers and I would rather hear a good one. 👇
Written with AI assistance. Every number, status code and command output above is from my own live checks on July 20, 2026, and the failing runs are printed as they came out.