firehose.compare methodology · ← leaderboard

Every 15 minutes the evaluator subscribes to all configured relays, observes a 10-minute window of #commit events, builds a cross-relay truth set of what verifiably happened, and scores each relay against it. Philosophy: truth = what verifiably happened (inclusive), scoring = fair aggregation (volume-capped), classification = why a miss happened (policy ≠ breakage).

1 · truth construction

Truth is the set of (did, rev) commit identities seen by at least K distinct quorum groups (default K=3) among the live references. A quorum group is the relay's backend if configured (relays that are fronts for the same physical backend count as one vote), else its operator, else the host itself — so one operator running many relays, or one backend behind many hostnames, can never vouch a commit into truth alone.

reference tightening

After a provisional truth set is built, each live reference's balanced coverage of it is computed. The survival threshold is relative, not absolute:

threshold = min(0.99, median(reference balanced coverage) − 0.015)

References at or above the threshold rebuild the final truth set. If fewer than K groups survive, the top K groups by balanced coverage are used instead — the evaluator never falls back to the loose (untightened) reference set, which historically let sub-1%-coverage relays vote on truth during spam bursts. Each run records truth_quality = tight or topk.

skew diagnostics

Each run stores truth_unique_dids and truth_top_did_share (the largest single DID's fraction of truth). The dashboard shows a red skew badge when the top DID exceeds 2% of truth — a single account dominating the window means raw coverage is measuring rate-limit policy, not relay quality.

2 · balanced coverage (headline)

Raw commit coverage is micro-averaged, so one account emitting 37k commits in 10 minutes can control 15 points of the score. The headline metric instead winsorizes each DID's contribution:

n_d      = number of truth commits for DID d
C        = max(20, p99 of n_d over all truth DIDs)   # winsorization cap
weight   = min(n_d, C) / n_d                          # per-commit weight for DID d
seen_d   = relay's truth-intersected commits for DID d

balanced = Σ_d ( seen_d · min(n_d, C) / n_d )  /  Σ_d min(n_d, C)

Balanced coverage equals raw coverage when no DID exceeds the cap. A spam DID at 37,000 commits per window contributes at most C commits to the denominator (≈0.01% of a typical window) instead of ~15%.

columnmetricdefinition
covbalanced coverageper-DID winsorized commit coverage (above) — the ranking metric
brdDID breadthfraction of truth DIDs from which the relay delivered ≥1 commit
rawcommit coverage|seen ∩ truth| / |truth|, uncapped — diagnostic only
p95freshness p9595th percentile of arrival − commit-TID time, ms (live commits only)
gapsactive missesmisses on live, resolvable PDSs with no policy/backlog explanation
polpolicy + backlogmisses explained by the relay's own host policy or ingest lag
t24h tiertier letter — see §5

supporting metrics

3 · grace window

Collection runs until WindowEnd + arrival_grace (default 10s — the maximum delivery latency tolerated of a reference). Events arriving in the grace tail still count toward the relay's seen-commit set (if their TID is in-window and they pass liveness) but not toward wall-clock counters or freshness. Without it, a slower-but-complete relay would carry a structural penalty of max(0, latency − edge) / 600 per run purely for delivery latency. The classifier additionally forgives misses whose TID falls within edge_seconds (default 10s, was hardcoded 2s) of the window boundary; with the grace window this bucket should be near zero and is kept as a guardrail.

4 · miss taxonomy

Every truth commit a relay did not deliver is classified:

bucketmeaningcharged?
edgecommit TID within edge_seconds of the window boundaryforgiven
activeresolvable DID, live PDS, relay at-head or unprobed — a genuine gapyes — "gaps"
policythe relay's own host policy explains the miss: PDS banned / not indexed / throttled / stalled / rate-limited (host-status probe)separated — "pol"
backlogrelay is ingesting the PDS but behind (seq advancing at a measured rate, behind head)separated — "pol"
deactivatedaccount deactivated/deleted per PLC at classification timeforgiven
unresolvableDID permanently unresolvable (e.g. tombstoned)forgiven
unclassifiedresolver rate-limited, resolution cap reached, or pending — never silently forgiven; shown in yellowreported

DID→PDS resolutions are cached persistently (7d TTL for active/deactivated, 6h for unresolvable); resolver 429/5xx responses are retried once with backoff, then land in unclassified rather than being miscounted as unresolvable. Network lookups are capped at 20,000 DIDs per run; misses beyond the cap are unclassified.

host-status probing

After each run, for every relay × its top-5 missed PDS hosts, the evaluator issues read-only GET /xrpc/com.atproto.sync.getHostStatus?hostname=<pds> requests against the relay — two samples ≈20s apart — yielding the relay's cursor (seq) into that PDS and its consumption rate. The cross-relay head seq for a PDS is the max seq any relay reports (seq spaces are comparable per host, never across hosts). Each (relay, pds) pair is classified:

classsignalbucket
bannedrelay reports status banned/blocked for the hostpolicy
not_indexed404 / HostNotFound — relay never crawled the PDSpolicy
throttledexplicit throttled/exhausted statuspolicy
stalledΔseq = 0 while >5000 behind headpolicy
limitedconsuming at <20% of the best observed rate while behindpolicy
pacingadvancing at a measured rate but behind headbacklog
at_headwithin 1000 of head — misses stay activeactive
behind_unknownbehind head but no rate evidence (a probe sample failed, or zero events during the gap) — neither forgiven nor charged as policyactive
unsupportedendpoint missing (older implementations)active
unknownboth probe samples failed, or no head evidence exists for the PDSactive

Implementation quirk: rsky relays report every host active, so status-based classes (banned/throttled) are never taken from an rsky status field — only seq evidence counts there. Each missed PDS is probed against every connected relay (not just the ones that missed it) so the head baseline comes from a relay that actually keeps up.

The drawer shows the probe verdict as a chip next to each missed PDS, e.g. ⊘ limited · 0.2 ev/s · 13.1M behind.

5 · tiers

Single-window ranks inside the top cohort are statistical noise (~13% of adjacent ranks swap run-to-run on <0.1pp differences). The tier column groups relays whose trailing-24h performance is indistinguishable: per relay, the median balanced coverage over its non-invalidated ok rows in the last 24h, with a bootstrap 95% confidence interval (1000 resamples). The tier score is availability-weighted: median × ok-fraction, where ok-fraction is the share of scheduled windows the relay was measurable in (quality ok, not invalidated). Without this, a relay whose bad runs go thin/invalidated — e.g. a cursor-lag oscillator delivering stale backfill every other window — would tier on only its good runs (survivorship bias). Relays sorted by weighted score; a relay joins the current tier if its (availability-scaled) CI overlaps the tier leader's, otherwise it starts the next tier. Order within a tier is not meaningful. A relay needs at least 8 runs (~2h) of history before it holds a tier — until then it shows · and sorts after tiered relays, so one clean cold-start run can't out-rank relays with real history.

6 · known artifacts

7 · notes

Runs recorded before 2026-06-12 predate some of these metrics: balanced coverage falls back to raw coverage, the policy/backlog/unclassified buckets are empty, live ratio carried a structural ~95.2% ceiling, and freshness percentiles may include backfill samples. Cross-era comparisons of those metrics are not apples-to-apples.

Not yet implemented: leave-one-out truth for references; #identity/#account/#sync completeness scoring; a per-PDS coverage matrix from full DID→PDS attribution of truth; freshness Apdex.