Some ideas have been sitting in the back of my notes for years, never quite reaching the front of the queue. This was one of them.
For a long time I've wanted asn.ipinfo.app to answer a question that sounds simple and isn't: how many domains actually live behind a given ASN? Not how many IPs — that's just announcement size. Domains. The thing humans actually interact with. A network announcing a /16 with twelve domains on it is a fundamentally different beast than one announcing the same space with twelve million.
The reason that question matters is what it lets you infer. Domain density per IP is one of the cleanest signals you have for whether a given ASN is a hosting provider, a residential ISP, a corporate or transit network, or something stranger. A residential ISP shows you huge address space and almost no domains pointed at it — eyeballs, not servers. A budget shared-hosting company shows you a handful of /24s and millions of domains crammed onto them. A transit-heavy carrier shows you neither. That kind of categorisation is genuinely useful for abuse work, blocklist tuning, and just understanding what an autonomous system is — and "domains per ASN" is the missing input for it.
Certificate Transparency seemed like the obvious source. Every public TLS certificate eventually ends up in a CT log. Subscribe to the CertStream firehose, reduce hostnames to registrable domains via the public suffix list, resolve those to IPs, look the IPs up against Atlas for an ASN, and write a (domain, ASN) pair into Postgres. Run that long enough and you have a self-updating count of every domain on the public internet, attributed to its hosting network.
The honest reason it stayed in the back of the notes for so long is that I haven't had the time. I work four ten-hour days, and once you add drive on either side they're closer to twelve. There isn't a lot of evening left over for a side project of this size, and the ones I do start tend to be small enough to finish on a Saturday.
Claude has been part of how I build for a while now, and this was another project where I leaned on it heavily — specifically because I knew I didn't have the runway to get this one off the ground on my own. A project this size needs more lift than my evening hours alone can give it; with an AI in the loop handling scaffolding and glue, the same short runway is suddenly enough to actually rotate. "I should try this someday" turned into "let's see how far the hours I actually have will take it."
Further than I expected. Not far enough.
First, the thank-you
Before I get into what broke, I owe a much bigger thank-you to a much more important person than the model.
I've been hosting with Catalyst Host for years — long enough that the relationship has outlasted most of the projects I've ever run on it. Ryan, who runs Catalyst, is the kind of host you stop comparison-shopping for, because you stop having a reason to. The boxes just work. The network just works. When something does need a hand, the response is fast and genuinely kind. You stop checking prices the moment you stop having to worry about the thing you're running on.
The whole CT-ingest pipeline came together in about a day with Claude. By the end of that day it was already saturating the box I had it pointed at. I mentioned that offhand — not as a request, just an observation that the experiment was going to need more compute if I wanted to keep poking at it. I did not ask. I would not have asked.
Ryan, unprompted, handed me eight cores of CPU to play with so I could keep going.
Eight cores. On an experiment that ultimately didn't ship.
That's who he is. If you need a VPS or dedicated server and want a real human on the other side who actually cares whether your stuff is working, this is a wholehearted recommendation: Catalyst Host. That link is an affiliate link, and I'm using it deliberately. If I'm going to send anyone his way, I'd like a sliver of it to come back his direction for everything he's already done for me over the years. He has more than earned it.
The eight cores got the pipeline closer than it had any right to be. They just couldn't bend the underlying math.
Where the math broke
CertStream isn't a polite stream. Public CT logs run somewhere between 600 and 1,300 certificates per second, with regular peaks above that. Most of those are noise from a domain-counting standpoint — renewals, new subdomains beneath domains you've already counted, short-lived ACME issuance — so after reducing to registrable domains and deduping in a hot set, you're still looking at roughly 150 to 300 genuinely novel domains per second that need real work done on them.
Real work, in this design, was three serial dependencies per novel domain — DNS lookup, ASN lookup against Atlas, then a Postgres UPSERT on the (domain, ASN) pair to update the count. Each step is fast on its own; the DNS and ASN sides scale by throwing more workers at them. The piece that doesn't scale that way, and the piece that ended up being the actual bottleneck, was Postgres.
Every novel domain ends up as a write. At 150–300 of them per second, sustained, on a single primary, you're asking one box to absorb every bit of work the upstream stages produce — index maintenance, WAL flushes, lock contention on hot rows, autovacuum trying to keep up with the churn. I tuned for the write-heavy pg-boss workload. I cranked the workers. I added the in-process LRU cache so hot IPs never even reached the ASN service. Each round of tuning pushed the bottleneck around for an hour and then handed it right back to Postgres. There was always more firehose than there was database, and once the queue starts falling behind, it stays behind — the firehose doesn't slow down to be polite.
The path forward from there isn't a mystery. It's just out of scope for a side project on personal infra:
- Multi-machine compute, with shards of CertStream traffic going to different worker pools instead of one.
- A sharded or partitioned Postgres, because a single primary is the wrong answer at this UPSERT rate.
- An in-process BGP table instead of per-IP HTTP ASN lookups — pull a full RIB into memory and answer ASN queries locally rather than over the wire.
Each one of those is its own project. Stacking three of them on top of an experiment whose only end-user-visible deliverable was a single new endpoint stopped being a sane trade.
Where Claude fits in
This isn't my first project with Claude — it's one of many at this point, and the reason I keep coming back is simple. With a handful of evenings a week to spend on side projects, I cannot ship the things I care about on my own time alone. Claude takes on the heavy lifting I'd otherwise be slogging through, and the hours I do have go a lot further.
Concretely, on this one: Claude got me to a working answer in a day. Pipeline built, running, ingesting, hitting the wall. If I'd written it myself I'd have spent weeks on it before I knew whether the idea was even feasible — and "weeks" is hard to find around a four-day, ten-hour-day work schedule. Getting the no fast is, frankly, just as valuable as getting a yes fast. At a hundred dollars a month, that's one of the easier subscriptions I pay for to justify. (And if you've ever found these projects useful, a small donation helps cover that subscription so the next experiment can happen.)
The one thing Claude won't do is tell you when to stop. It'll happily help you build something all the way to the wall. Knowing when the wall is the wall is still the human's job.
What I'm keeping
The whole experiment is in the git history if I ever want to revive it. What actually came out of the run, though:
- A calibrated sense of how unforgiving CT firehose ingestion is at sustained rates, and a real feel for the hardware envelope the obvious design closes inside of.
- A reusable Docker Compose pattern for write-heavy worker pipelines: self-hosted Unbound for outbound DNS isolation, a tuned write-heavy Postgres, pg-boss for the queue, a producer/worker split that keeps the firehose loosely coupled from the slow path. I'll use this shape again.
- One incidental finding worth its own footnote: the public Calidog CertStream endpoint is dead.
wss://certstream.calidog.io/doesn't accept connections anymore — and reasonably so. The full feed is something like 50 Mbps of sustained traffic per subscriber; running that as a free public service is a meaningful bandwidth bill, and I don't fault anyone for turning it off. The fix is to self-host: pull d-Rickyy-b/certstream-server-go (Docker image0rickyy0/certstream-server-goon Docker Hub), run it as a sidecar, and point your client at the local/full-streamendpoint instead of the public one. It connects to CT log operators directly and gives you the same firehose Calidog used to serve. Only cost me an hour or so to figure out, but it's the sort of thing I wish someone had blogged about a year ago — so consider this that blog post. - An opinionated, short list of what a feasible next attempt would need: in-process BGP RIB instead of HTTP ASN lookups, sharded ingest, partitioned Postgres. If I ever come back to this, that's the starting line.
The Atlas /api/v2/asn/:as_number/popularity endpoint that this pipeline was meant to feed is gone, along with the rest of ct-ingest.
This isn't the end
I'm framing this as a stopping point, not a graveyard.
The question — how many domains actually live behind every ASN? — is still a good question. It's still one I want asn.ipinfo.app to answer. The single-VM, HTTP-everywhere, real-time-ingest pipeline just isn't the way there. Something colder, more batched, and less ambitious about freshness probably is — passive DNS aggregations, periodic cold-pulls from archived CT logs, a once-a-day batch reconciler that doesn't care about firehose semantics. Any of those is a more reasonable starting line for the next attempt than what I built this round.
I don't know yet which one I'll come back to. I do know I'll come back to it eventually, because the question hasn't gone anywhere — and neither has the audience that would find the answer useful.
Until then: thank you, Ryan, for the cores, the years, and for being the kind of host who makes experiments like this possible at all. The pipeline didn't ship. The relationship is the part that mattered.
If you've made it this far and you're hosting-shopping, give Catalyst a look. You won't regret it.