# How iroh Measures and Reports Network Performance Using NetReport

> Learn how iroh measures and reports network performance with NetReport. Discover its three-stage pipeline probing QUIC and HTTPS endpoints for aggregated results and async updates.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-07-05

---

**Iroh continuously evaluates network conditions through a three-stage pipeline that probes QUIC and HTTPS endpoints, aggregates the results into a structured `Report`, and publishes updates via an async `Watcher` API.**

The NetReport subsystem in the [n0-computer/iroh](https://github.com/n0-computer/iroh) repository provides real-time visibility into NAT behavior, public IP addresses, and relay latency. This Rust-native implementation enables peer-to-peer applications to adapt dynamically to changing network topologies without blocking the main event loop.

## The Three-Stage NetReport Pipeline

Iroh's network measurement process consists of tightly coupled stages for probing, aggregation, and publication. Each stage operates asynchronously and can be traced through specific source files in the `iroh/src/net_report/` directory.

### Stage 1: Probe Planning and Execution

When an `Endpoint` initializes, it spawns a `net_report::Client` that builds a **probe plan** based on current interface state (`IfStateDetails`) and configuration (`NetReportConfig`). The client executes two distinct probe types in parallel:

- **QAD (QUIC Address Discovery) probes** – Open QUIC connections to relay servers for IPv4 and IPv6, capturing the observed external address and measuring round-trip time (RTT).
- **HTTPS latency probes** – Execute empty GET requests against each relay’s `/generate_204` endpoint to obtain lightweight latency measurements.

The probe execution is driven by `net_report::reportgen::Client` and helper functions `run_probe_v4` and `run_probe_v6` defined in [`iroh/src/net_report.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report.rs) (lines [57-66](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report.rs#L57-L66) and [92-124](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report.rs#L92-L124)).

### Stage 2: Report Aggregation

Each probe yields a `ProbeReport` that feeds into the mutable `Report` struct via the `Report::update` method in [`iroh/src/net_report/report.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/report.rs) (lines [67-130](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/report.rs#L67-L130)). This aggregation logic:

- Records per-relay latency in `relay_latency`
- Sets `udp_v4` and `udp_v6` flags upon successful QAD completion
- Captures public addresses as `global_v4` and `global_v6`
- Detects address-mapping variance to identify NAT behavior
- Flags `captive_portal` when `/generate_204` returns a non-204 response

### Stage 3: Real-Time Publication via Watchers

The endpoint stores the final `Report` in a `Watchable<(Option<Report>, UpdateReason)>` field inside the socket state. After `Client::get_report` completes, the associated `Watcher` notifies all consumers. Applications obtain a watcher through `Endpoint::net_report()` (defined in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) lines [1330-1335](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs#L1330-L1335)), enabling reactive network monitoring without polling.

## Key Source Files and Architecture

Understanding the following files is essential for customizing or debugging network measurements:

- **[`iroh/src/net_report.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report.rs)** – Core `Client` implementation handling probe orchestration, history tracking via `add_report_history_and_set_preferred_relay`, and the `FULL_REPORT_INTERVAL` (5-minute) scheduling logic.
- **[`iroh/src/net_report/report.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/report.rs)** – Definition of the `Report` struct with fields like `preferred_relay`, `global_v4`, and methods like `has_udp()` for checking UDP reachability.
- **[`iroh/src/net_report/reportgen.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/reportgen.rs)** – Actor-based probe generation that manages concurrent HTTPS requests and QUIC handshakes.
- **[`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)** – Public API entry point exposing `net_report()` to return a `Watcher`.
- **[`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs)** – Maintains the `net_report` field as a `Watchable` state container.
- **[`iroh/src/metrics.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/metrics.rs)** – Tracks `NetReportMetrics` distinguishing full versus incremental reports.

## Implementing NetReport in Your Application

The following examples demonstrate how to consume network reports in Rust applications using iroh's async API.

### Retrieving the Initial Report

Use `initialized()` to await the first completed network report before proceeding with peer connections:

```rust
use iroh::{Endpoint, endpoint::presets};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let ep = Endpoint::bind(presets::N0).await?;
    
    // Blocks until the first report is ready
    let report = ep.net_report().initialized().await;
    
    if let Some(r) = report {
        println!("Public IPv4: {:?}", r.global_v4);
        println!("Public IPv6: {:?}", r.global_v6);
        println!("Preferred relay: {:?}", r.preferred_relay);
        println!("UDP reachable: {}", r.has_udp());
    }
    Ok(())
}

```

### Subscribing to Continuous Updates

For long-running applications, treat the report as a stream to react to network changes in real time:

```rust
use iroh::{Endpoint, endpoint::presets};
use futures_util::StreamExt;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let ep = Endpoint::bind(presets::N0).await?;
    let mut watcher = ep.net_report();
    
    while let Some(opt) = watcher.next().await {
        if let Some(r) = opt {
            println!("Network changed - new preferred relay: {:?}", r.preferred_relay);
        }
    }
    Ok(())
}

```

The `Watcher` remains valid for the `Endpoint` lifetime, making it suitable for background health monitoring tasks.

### Analyzing Per-Relay Latency

Inspect individual probe results to build custom relay selection logic:

```rust
if let Some(report) = ep.net_report().initialized().await {
    for (probe, relay, latency) in report.relay_latency.iter() {
        println!("{:?} probe to {} took {:?}", probe, relay, latency);
    }
}

```

The `RelayLatencies::iter` method (defined in [`iroh/src/net_report/report.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/report.rs) lines [80-86](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/report.rs#L80-L86)) exposes raw timing data for both QAD and HTTPS probes.

## Summary

- **NetReport** operates through three stages: probe execution (QAD/HTTPS), result aggregation via `Report::update`, and publication through `Watcher` streams.
- **Source locations** – Core logic resides in [`iroh/src/net_report.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report.rs), [`report.rs`](https://github.com/n0-computer/iroh/blob/main/report.rs), and [`reportgen.rs`](https://github.com/n0-computer/iroh/blob/main/reportgen.rs), with public API exposure in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs).
- **Probe types** – QUIC address discovery probes determine public IPs and UDP viability, while HTTPS probes measure baseline relay latency.
- **Access patterns** – Use `initialized()` for blocking initialization or `Watcher::next()` for continuous monitoring of network topology changes.
- **Report contents** – Each `Report` contains `global_v4`/`global_v6` addresses, `preferred_relay` selection, `relay_latency` mappings, and NAT detection flags.

## Frequently Asked Questions

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

**QAD (QUIC Address Discovery) probes** establish actual QUIC connections to measure RTT and discover the host's public IP address through STUN-like mechanisms, while **HTTPS probes** perform lightweight HTTP GET requests to `/generate_204` endpoints solely to measure latency without establishing persistent connections. QAD probes validate UDP reachability and NAT behavior, whereas HTTPS probes provide a cheap fallback for latency comparison when QUIC is blocked.

### How often does iroh generate NetReport updates?

By default, iroh schedules full reports every **5 minutes** (`FULL_REPORT_INTERVAL`). However, the system triggers **major** reports immediately upon detecting interface state changes through `network_change()` events. Incremental updates may occur more frequently when the `Client` determines that existing probe data is insufficient via the `have_enough_reports` check.

### Can I access network reports without blocking my application?

Yes. While `initialized()` blocks until the first report completes, you can obtain a non-blocking `Watcher` immediately via `Endpoint::net_report()` and poll it asynchronously using `.next().await` in a background task. The watcher yields `Option<Report>` where `None` indicates that a report is still in progress, allowing your application to continue operating with degraded network awareness rather than waiting.

### How does iroh determine the preferred relay server?

After each report completes, iroh calls `add_report_history_and_set_preferred_relay` to merge new latency data with historical measurements. The relay with the lowest recent latency across both QAD and HTTPS probes becomes the `preferred_relay` stored in the `Report`. This selection updates dynamically as network conditions change, ensuring optimal connection paths for peer-to-peer communication.