Node census logbook

About this text

Claude, a language model, wrote it from the commits, work journals and measurements in the project’s repositories. The work — building the crawler, breaking each ceiling, measuring the spend, getting things wrong — is a human’s, and he verified the numbers that appear here.

A reader pointed out that the previous version “sounds like an LLM answer”. He was right. The response was not to disguise it better but to declare it, and to change the format to one where machine authorship does not get in the way: a logbook. There is no thesis to defend and no prose to admire here, only dated entries with what was observed, what was changed, and the number that came out.

The piece with analysis, opinion and a human byline will come separately.


Before the record (May – July 2026)

Starting point: a fork of ayeowch/bitnodes, the code behind the original bitnodes.io, unmaintained upstream.

Ceiling 1: ~1,390 nodes. A 2 vCPU box. One Tor daemon pinned at 100% CPU was dropping close to a million circuit requests every ten minutes. Snapshots swung between 55 and 1,384. Detuning the crawler settled it at ~1,390. It held for weeks.

Ceiling 2: ~4,170 nodes. Resize to 8 vCPU. The count tripled and then sat still for two months. Nothing in the data suggested an artifact.

2026-07-16 — Composition gives the artifact away

Of the ~4,170 nodes, 12 were onion. The gap against the ~20,000 another tracker reported was almost entirely .onion. The 25% sampling and a single saturated Tor came from mitigating a May incident and had never been revisited after the resize.

2026-07-17 — Six-daemon Tor pool, and the ceiling does not move

Onion sampling to 100% and six Tor daemons. Onion: 12 → 226 in two hours. Total: still ~4,170. Every onion gained displaced an IPv4, one for one.

That 1:1 was the number that explained everything: a snapshot can only hold as many nodes as there are simultaneously open sockets, i.e. ping processes × workers per process. 7 × 600 = 4,200, against 4,170 observed.

Raised to 12 × 2,000 = 24,000 slots. Result in five hours: 11,600 nodes, with the IPv4 population (~6.4k) complete for the first time.

2026-07-18 — I2P does not need bandwidth, it needs seeds

After 16 hours with I2P support enabled: zero I2P nodes. The pending queue held zero .b32.i2p addresses. Clearnet peers almost never gossip I2P addresses, so the ring does not bootstrap on its own.

Seeded 512 destinations from Bitcoin Core’s hardcoded seeds. The queue jumped to ~3,900 pending. Reachable nodes: still zero.

2026-07-19 — The bug that made I2P invisible

Dial attempts instrumented. The address serialiser classified .b32.i2p as IPv4 because it contains dots, and called inet_pton(AF_INET, …), which blew up the handshake right after the SAM stream opened.

Every I2P connection died in a way that, from outside, looked like “there are no I2P nodes”. Fixed: addrv2 gets its 32 bytes and the legacy version field a null address, as Bitcoin Core does. Result: ~4,700 reachable I2P nodes, a population almost no public tracker counts.

2026-07-19 — Tor is single-threaded

Onion had been decaying for days. ps showed six Tor daemons at 475% CPU and looked like saturation. A full-day sar showed the machine at 36% idle.

It was not saturating: each Tor daemon is single-threaded and tops out at one core no matter how big the box. The lever is more daemons, not a bigger machine. Moved to nine.

2026-07-19 — Instantaneous and windowed counts measure different things

Added the rolling union of unique nodes per network, which is how the reference tracker counts. Over an 8-day window: 27,063 unique nodes against their 22,232. Tor connections are transient, so an instantaneous snapshot undercounts onion by construction.

2026-07-21 — The disk fills and takes I2P down with it

Disk at 94%. Three things converged on a 15 GB volume: debug logs inflated to 3.5 GB, 4.6 GB of orphaned Redis temp files from May, and larger snapshots after adding I2P.

i2pd went down, and the SAM bridge with it: I2P to zero. Tor lost its ability to cache the consensus: onion to 18.

2026-07-22 — A silent 21.5-hour freeze

The dashboard served a 21.5-hour-old snapshot without anything “failing”: every process alive, every systemd unit green.

Cause: the crawler’s restart() sent a transaction of ~123,000 Redis commands in a single multi-megabyte send(). Under load it stalled until TCP gave up after ~16 minutes, and the uncaught exception killed the scheduler greenlet right after it had set the state to “starting”. The 1,200 workers paused politely, forever.

Diagnosis: py-spy cannot see greenlets, so it took injecting code into the live process with gdb to dump the 1,200 stacks.

Lesson recorded: monitor the age of the last export, not process health.

2026-07-23 — Tor’s guard anti-DoS throttles crawlers

After a restart, onion would not ramp: 17 → 21 → 0 over a day, with the Tor daemons nearly idle. The logs: 247,230 expired circuits against 212 completed, with “Guard is failing more circuits than usual”.

A crawler funnels all circuit creation through one or two entry guards per daemon, which is exactly the pattern the per-IP anti-DoS defence throttles. With no anonymity requirement, the fix is one torrc line: UseEntryGuards 0.

Under the same load: onion from 3 to 2,532 in two hours, plateauing thirteen hours later at ~10,700-10,800 simultaneous onion connections. Previous record: 3,394. Total at plateau: ~22,100 nodes.

2026-08-01 — The egress bill is another ceiling

The cloud provider’s cost anomaly detection flagged the instance: data transfer 2,307% above expected, a flat 330-350 GB/day, 24/7. The profile read as “instance compromised and used as a proxy or relay”, and the machine ended up quarantined. The dashboard went down.

False positive: the “malware” was the crawler. The ramp matched the scaling work of the previous stages to the day.

Daily July egress with the stages annotated

Measuring the real split, via per-process sent-byte deltas over 60-second windows, cross-checked against the i2pd router console:

Egress composition: 55% I2P, 45% Tor, 0.1% Bitcoin protocol

More than 99% of the bill is overlay machinery: I2P tunnel building at a 67% success rate, leaseset lookups, NetDb maintenance, and on the Tor side about 1,000 fresh TLS connections per minute, the direct price of UseEntryGuards 0. Transit for third parties: zero. Bitcoin protocol traffic is a rounding error.

Every stage left its signature on the cost curve: $0.30/day before scaling, $1.60/day with the full socket budget, $9-15/day with I2P and the nine Tor daemons, and $22/day from the exact day UseEntryGuards 0 landed.

Egress cost against reachable nodes, log scale

At $0.09/GB that is **$660/month in transfer alone**. It is a hyperscaler phenomenon, not a “cloud” one: those same ~10.7 TB/month are included in a €20 European VPS, and irrelevant on your own hardware. Decision taken that day: migrate off. Crawler stopped in the meantime.

2026-08-07 — Retraction: the unique-node metric was wrong

Since July the dashboard had published a “unique nodes” estimate weighting each reachable address 1/N, where N was the number of network types that node advertised. Withdrawn. The endpoint now answers 410 with the reason.

Two things were wrong. The input: N came from the keys that cache the response to GETADDR, i.e. the addresses a peer knows about other nodes, not its own. It measured the diversity of its address book. Since Bitcoin Core stores onion and I2P addresses whether or not it can dial them, a clearnet-only node with a varied address book was weighted 1/3 or 1/4 and counted as a fraction of a machine. The estimate deflated silently and the numbers looked plausible throughout.

And fixing the input would not have saved it: deduplicating across networks requires linking a node’s IPv4 to its .onion or .b32.i2p, and that link is never published. Core self-advertises with an address belonging to the network of the peer it is talking to. Unlinkability is the design goal of Tor and I2P.

There is no deduplicated node count on the dashboard, and there should not be one on anyone else’s. The comparable figure is the windowed one, which counts distinct addresses per network and makes no claim to count machines.

2026-08-13 / 15 — A/B test: MaxCircuitDirtiness

A reader suggested that raising Tor’s MaxCircuitDirtiness would cut handshake churn. The manual backs the mechanism: for hidden services the expiry is measured from the last use, not the first, so the 10-minute default is an idle timer. The crawler revisits each onion every 30 minutes, so every cycle found every circuit expired.

Testing it required fixing something first: the crawler picked a Tor daemon at random on every dial, so a revisit met the warm circuit one time in nine. Each onion address now always goes to the same daemon, chosen by a stable hash.

A 38-hour run with all eight daemons started cold at once, four treated with MaxCircuitDirtiness 3600 and four control at the default. Comparison over 25.4 hours of plateau and 11,832 samples:

egresssustained connsbytes per conn
treated28.65 MB/min~2,43512,338
control35.52 MB/min~5,7646,462
delta-19.4%-57.8%+90.9%

Conclusion: do not raise MaxCircuitDirtiness on a crawler. The 10-minute default wins.

The -19.4% egress is not a saving: the treated arm spent less because it did less work, not because it was more efficient. It carried 58% fewer connections. Measured per unit of work — the only basis on which costs can be compared — it came out twice as expensive.

The reason: holding circuits for an hour leaves about 6,400 open per daemon against 3,900, and that occupancy throttles throughput. Each treated daemon carried less than half the streams of a control one.

What the run did prove is the mechanism that motivated it, and it is large: by Tor’s own counters, a treated daemon opened 1,747 new outgoing connections per hour against the control’s 4,686, 63% less handshake churn. The idea was right; the balance at this value is not.

So what is discarded is this setting, not the idea. For a circuit to survive the revisit, the expiry has to exceed the cycle’s 30 minutes: below that nothing is reused and the behaviour collapses back to the default. The gap worth exploring is narrow and specific — 35 or 40 minutes, just above the cycle but far from an hour — to see whether it keeps part of that 63% without throttling throughput. Untested so far.

What still stands

  • A tracker’s reachable node count is a property of the tracker. Every ceiling in this logbook looked like the network until it broke.
  • Comparing trackers is comparing infrastructures. IPv4 figures converge because IPv4 is cheap to enumerate; onion and I2P figures diverge because they depend on circuit capacity, guard throttling and seed availability.
  • Overlay visibility has a price, and it is not paid in Bitcoin bytes.
  • If the count plateaus, before concluding “that’s the network”: composition, socket budget, upstream throttling.
  • And a published measurement is only worth as much as the method stated beside it. The 7 August entry is the proof.

What is in the fork

  • I2P as a fourth ring — a SAM v3 client written from scratch, I2P branches through crawl/ping/resolve, and destination seeding.
  • The I2P handshake fix — the actual reason I2P reported zero.
  • Multi-Tor pool — multi-line tor_proxies, with per-address affinity since August.
  • Pipeline reliabilityrestart() split into batches instead of one multi-megabyte transaction, plus a supervised scheduler.

Live instance: pesquisa.hacknodes.xyz (dashboard, REST API, MCP server). Code: alt-bitnodes and the crawler fork, both MIT. Raw snapshots and the historical archive are published.

Happy to have someone dispute the numbers.


alt-bitnodes is an independent open source project (MIT) maintained by the author. HackNodes Lab is his security consultancy; the observatory is not a commercial product.