Technical correction — October 20, 2026: Terminology and counter interpretations have been corrected. The original observation dates are retained.
Part 5 of a five-part series on the June–July 2026 circuit-building DoS. 1. The Tor Network’s Circuit-Building DoS Wave in June–July 2026 — the network-wide view from Tor's public data. 2. Anatomy of the Attack — the four observed phases in the updated reconstruction. 3. What It Actually Costs — the CPU and memory bill, and the public comparisons. 4. The Flood Below Tor — the TCP listener pressure Tor's own counters cannot see. 5. Defending Against Circuit-Building DoS (you are here) — better metrics and the protocol fixes we're asking for.
The circuit-building DoS wave hitting the Tor network made two things clear on our own guard-relay fleet: we needed to see the attack more clearly, and the durable fix belongs in the Tor protocol, not on the operator's server. This post covers both — first how we're improving our own monitoring in a way that adds zero logging and touches no personal data, then the specific things we think the Tor Project could change.
The obvious way to study an attack is to record what's arriving: capture connections, log source addresses, inspect traffic. We do not do that, and we are not going to. Everything in our companion posts — CPU, memory, onionskin queues and defense actions — comes from aggregate counters, never a packet capture or a tally of source addresses. A large distributed source pool is one possible explanation, but the counters do not establish its size, origin paths or common control. Per-source defenses have activation conditions and per-process scope, and circuits can accumulate over time below a rate limit.
Our monitoring principle is simple: measure the relay's own health and the attack's aggregate shape, never the individual source. Concretely, that means we build only on two kinds of source, both aggregate by construction:
tor_relay_*) — counters and gauges the Tor process already computes: onionskins processed and dropped, circuits opened, DoS-defense actions taken, accounted bytes removed by the memory handler. These are totals, not events; there is no per-source or per-connection record to leak.node_exporter) — CPU utilization, free memory, and the kernel's own OOM-kill counter. Machine health, nothing about traffic.Where we want more visibility, we get it by adding more aggregate signals, and by correlating them against public data (the CollecTor archives and Onionoo) rather than against anything we'd have to record ourselves. The gaps this wave exposed, and how we're closing them privacy-safely:
| What we couldn't see well | The privacy-safe fix |
|---|---|
| Per-relay resident-memory growth | Export each relay process's resident-memory total (one gauge per relay). It's a number of bytes, not a record of anything. |
| Which host recorded onionskin queue-overflow drops, and when | Scrape the existing onionskins_total{action="dropped"} counter per relay — and never infer drops from the Heartbeat assigned/requested gap (see Part 2), which is not a drop count. |
| Whether kernel memory pressure was turning into outages | Watch node_memory_MemAvailable_bytes and node_vmstat_oom_kill at the host level, alongside Tor's metrics — the OOM story is invisible to a Tor-only dashboard. |
| How our standing compared to the rest of the network | Correlate our aggregate health against the public CollecTor/Onionoo record. The comparison lives in public data, so we never have to build a private one. |
| How large or coordinated the attacking source pool was | We do not count or bucket source addresses. Aggregate circuit counts and rejection activity cannot determine source-pool size, whether it was distributed, or whether the same actor controlled activity across hosts. |
Two metrics fooled us early, and both are easy traps for any relay operator. We publish them because the correction is more useful than the mistake.
Tor's Heartbeat prints a line like Circuit handshake stats since last time: ... 41000/42700 NTor, .... Reading the pair as assigned/requested is correct; calling the gap "dropped onionskins" is not. In the cited Tor source, requested increments after parsing and early rejection checks; assigned increments before worker dispatch, which MetricsPort exposes as action="processed". The separate queue-admission rejection counter supplies action="dropped". All three omit channels Tor classifies as clients, including unauthenticated bridge connections. The requested-minus-assigned gap can include queue rejections, pending or abandoned work and timing effects; it does not identify the dominant cause or count successful circuit completions.
Use tor_relay_load_onionskins_total{action="dropped"} to track the recorded onionskin queue-overflow drops. The warning "Your computer is too slow to handle this many circuit creation requests." indicates queue pressure, but warning logs are rate-limited and their absence does not establish zero drops. Neither the counter nor the Heartbeat gap counts every failed or closed circuit.
Our inbound-connection gauge (tor_relay_connections{state="opened", direction="received"}) declined between restarts while host established-TCP counts stayed flatter. The connection-gauge investigation covers the accounting issue. Use tor_relay_connections_total{state="created"} for connection-creation activity and host TCP gauges for current connection levels; a cumulative counter is not a connection-level gauge.
Circuit handshakes consume CPU and idle circuits hold memory. Tor's existing mitigations rejected work during the event, but per-source controls are not a single aggregate limit on a relay's workload. The counters do not establish how many originators supplied that workload. These are design proposals for the Tor Project to evaluate, with attention to effects on legitimate users:
DoSCircuitCreationRate=2, DoSCircuitCreationBurst=60 and DoSCircuitCreationMinConnections=2. Operators can override them. The defense tracks addresses on client-classified channels per process and can mark an address when its token bucket is exhausted and the minimum-connection condition is met. Other channels are outside this rate defense. An aggregate design must account for relay peers carrying many users' circuits.DoSConnectionMaxConcurrentCount is enforced per Tor process; the sampled historical consensuses specify a threshold of 50. A host with many instances therefore has no host-wide cap from this setting alone. Any shared accounting design should consider clients behind shared addresses and avoid logging their identities.circ_proto_violation aggregates protocol-error circuit-close paths; it does not identify an original attacker. A relay's immediate peer can be another relay carrying many users' circuits. Any penalty needs to distinguish attributable direct-client behavior from relay-peer traffic and account for the specific violation path. The existing DoSCircuitCreation defense already excludes non-client channels. The recorded violation surge remains private MetricsPort evidence, absent from CollecTor and Onionoo. Publishing a suitably aggregate error signal could improve observability without claiming source attribution.ListenOverflows / ListenDrops record listener events, not unique completed connections. Retries can contribute repeated events, and drops can occur at different handshake stages. These host counters alone do not identify a Tor bottleneck or justify a connection-loss percentage. Exposing listener pressure alongside relay metrics would help operators see this layer without logging traffic; The Flood Below Tor explains the correction. Changes to acceptance or TLS scheduling need profiling and a separate implementation review.MaxMemInQueues covers several managed pools, including cell queues and onion-service descriptor-cache allocations; it is not a total-RSS limit. Allocator-retained memory and other allocations can therefore contribute to kernel OOM kills even when Tor's accounting stays below its limit. The later memory-defense analysis distinguishes the June/early-July behavior from the July queue-pressure episodes.We're running relays, publishing data, and comparing notes with other operators — but the durable fix belongs in the protocol. We are glad to share raw, aggregate data with Tor Project developers. Reach out.