# How Do Iroh Network Report Probes Work: QUIC, HTTPS, and Relay Selection

> Discover how Iroh network report probes use QUIC and HTTPS to measure relay reachability and latency, selecting the best connection for your needs.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: deep-dive
- Published: 2026-07-11

---

**Iroh network report probes measure reachability and latency to relay servers by executing a planned sequence of HTTPS and QUIC Address Discovery (QAD) probes, then aggregating the results to select the lowest-latency relay while accounting for NAT behavior.**

Iroh continuously evaluates network conditions to optimize peer-to-peer connectivity through relay servers. The **iroh network report probes** system generates periodic reports by orchestrating asynchronous HTTPS and QUIC probes, analyzing the results, and maintaining a history of relay performance. This mechanism, implemented in the `n0-computer/iroh` repository, enables intelligent relay selection based on real-time latency measurements and observed external addresses.

## Probe Planning: Creating the ProbePlan

The probe process begins in [`iroh/src/net_report/probes.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/probes.rs) where the `ProbePlan::initial` function constructs a deterministic schedule of measurements for every relay in the `RelayMap`.

The `Probe` enum defines the three supported probe types:

```rust
pub enum Probe {
    Https,
    #[cfg(not(wasm_browser))] QadIpv4,
    #[cfg(not(wasm_browser))] QadIpv6,
}

```

For each relay, the planner creates a **ProbeSet** containing multiple attempts. HTTPS probes are scheduled with three retry delays calculated as `HTTPS_OFFSET + DEFAULT_INITIAL_RETRANSMIT * attempt`, producing intervals of 200 ms, 300 ms, and 400 ms. When QUIC probing is enabled via `options.quic_config`, additional QAD sets are appended. The plan stores these sets in a `BTreeSet<ProbeSet>` to ensure deterministic ordering and automatic deduplication.

## Probe Execution: Running HTTPS and QUIC Probes

Execution occurs in [`iroh/src/net_report.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report.rs) where the `spawn_qad_probes` function manages concurrent probe tasks using a Tokio **JoinSet**.

**QUIC Address Discovery (QAD)** probes execute via `run_probe_v4` and `run_probe_v6`. These functions resolve the relay’s IP address using `reportgen::get_relay_addr_ipv4/ipv6`, establish a QUIC connection via `quic_client.create_conn`, and monitor the observed external address:

```rust
let conn = quic_client.create_conn(relay_addr.into(), host).await?;
let mut watcher = conn.observed_external_addr();
let addr = watcher.next().await.ok_or(QadProbeError::ReceiverDropped)?;

```

The initial `QadProbeReport` returns immediately, while a background task pushes subsequent address updates into a `Watchable<Option<QadProbeReport>>` to detect NAT changes during the reporting period.

**HTTPS probes** perform an empty GET request to `/generate_204` on each relay, measuring the round-trip latency.

The system limits concurrency to **MAX_RELAYS** (5) per IP version, wrapping each probe in a `PROBES_TIMEOUT` duration and a cancellation token for clean shutdown:

```rust
v4_buf.spawn(
    cancel_v4.child_token()
        .run_until_cancelled_owned(time::timeout(PROBES_TIMEOUT,
            run_probe_v4(relay, quic_client, dns_resolver, inner_token)))
        .instrument(info_span!("QADv4", %relay_url)),
);

```

## Report Aggregation and Relay Selection

The `get_report` function in [`iroh/src/net_report.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report.rs) receives probe results via a channel (`probe_rx`) and monitors QAD streams (`qad_conns.watch_v4/v6`). After each result, `have_enough_reports` checks whether the report is complete—requiring at least one IPv4 and one IPv6 QAD result when both stacks are present, or HTTPS results per relay.

Once sufficient data is collected, `add_report_history_and_set_preferred_relay` merges current latency measurements with a rolling five-minute history. It selects the **preferred_relay** with the lowest latency while applying a **hysteresis** rule to avoid unnecessary switching:

```rust
if prev_relay.is_some()
    && r.preferred_relay != prev_relay
    && !old_relay_cur_latency.is_zero()
    && best_any > old_relay_cur_latency / 3 * 2
{
    r.preferred_relay = prev_relay;
}

```

This logic retains the current relay unless the alternative is significantly faster (less than two-thirds of the current latency).

## Code Example: Generating a Network Report

The following Rust example demonstrates how to initialize the reporting client and request a network report:

```rust
use iroh::net_report::{Client, Options, Report};
use iroh_relay::RelayMap;
use iroh_dns::dns::DnsResolver;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;

#[tokio::main]
async fn main() {
    // 1️⃣ Build a RelayMap (e.g. from a list of known relays)
    let relay_map = RelayMap::empty(); // replace with real relays

    // 2️⃣ Create DNS resolver and TLS config (required for QUIC)
    let dns = DnsResolver::new();
    let opts = Options::default(); // enables HTTPS + QUIC probes

    // 3️⃣ Initialise the client
    let mut client = Client::new(dns, relay_map, opts, Default::default());

    // 4️⃣ Request a network report
    let if_state = client::net_report::IfStateDetails::default(); // detect IPv4/IPv6 capability
    let cancel = CancellationToken::new();
    let report: Report = client
        .get_report(if_state, /*is_major=*/ true, cancel.child_token())
        .await;

    println!("Preferred relay: {:?}", report.preferred_relay);
    println!("Relay latencies: {:?}", report.relay_latency);
}

```

Running this code initiates the probe plan, fires HTTPS and QUIC probes concurrently, and outputs the selected relay and measured latencies.

## Key Source Files and Structures

- **[`iroh/src/net_report.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report.rs)** – Core client implementation containing `get_report`, `spawn_qad_probes`, `have_enough_reports`, and `add_report_history_and_set_preferred_relay`.
- **[`iroh/src/net_report/probes.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/probes.rs)** – Defines `Probe`, `ProbeSet`, and `ProbePlan::initial` for scheduling probes with retry delays.
- **[`iroh/src/net_report/reportgen.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/reportgen.rs)** – Drives asynchronous probe execution and forwards results to the client.
- **[`iroh/src/net_report/report.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/report.rs)** – Data structures (`Report`, `RelayLatencies`) storing the final network report output.
- **[`iroh/src/net_report/defaults.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/defaults.rs)** – Timeout constants including `PROBES_TIMEOUT` and `DEFAULT_INITIAL_RETRANSMIT`.

## Summary

- **ProbePlanning** creates a deterministic schedule of HTTPS and QAD probes for each relay with calculated retry delays.
- **ProbeExecution** spawns up to 5 concurrent QUIC probes per IP version and HTTPS probes, using Tokio **JoinSet** with cancellation tokens and timeouts.
- **QAD probes** discover external addresses by opening QUIC connections and watching `observed_external_addr()` for NAT detection.
- **ReportAggregation** determines completion via `have_enough_reports`, then selects the preferred relay using latency history and hysteresis to prevent flapping.
- The system runs full reports every 5 minutes and on-demand for first connections, providing continuous network condition monitoring.

## Frequently Asked Questions

### What is the difference between HTTPS and QUIC probes in iroh?

HTTPS probes measure basic reachability and latency by issuing an empty GET request to `/generate_204` on the relay server. **QUIC Address Discovery (QAD) probes** open an actual QUIC connection to discover the node's external IP address and port, enabling NAT detection and endpoint mapping. While HTTPS probes provide simple latency metrics, QAD probes reveal how the network translates internal addresses.

### How does iroh decide when to stop probing?

The `have_enough_reports` function evaluates whether the current report contains sufficient data for decision-making. According to the implementation in [`iroh/src/net_report.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report.rs), the report is considered complete when it contains at least one IPv4 and one IPv6 QAD result (if both stacks are available) or adequate HTTPS probe results per relay. Once satisfied, the probe actor drops, cancelling remaining probes via the shutdown token to conserve resources.

### How does iroh select the preferred relay?

The `add_report_history_and_set_preferred_relay` function maintains a rolling five-minute history of latency measurements. It selects the relay with the lowest recent latency as the new preferred relay. However, it applies a **hysteresis** rule: if the current relay is performing adequately, the system will not switch to a new relay unless the alternative's latency is less than two-thirds of the current latency (`best_any > old_relay_cur_latency / 3 * 2`). This prevents rapid switching between similarly-performing relays.

### What happens when NAT changes during probing?

Because QAD probes return the initial `QadProbeReport` immediately but continue monitoring via a `Watchable<Option<QadProbeReport>>` in the background, the report generator can detect address changes during the probe period. If the external address observed over the QUIC connection changes, the background task updates the watchable, and `get_report` incorporates these updates into the final report, ensuring the network state reflects current NAT mappings rather than just initial discoveries.