# How to Debug iroh Connection Issues Using Path Events and Connection State Inspection

> Debug iroh connection issues with path events and state inspection. Monitor real-time path changes and inspect active routes to trace why QUIC connections stall or drop.

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

---

**Use `conn.path_events()` to monitor real-time path changes and `conn.paths()` to inspect the current snapshot of active routes, allowing you to trace exactly why a QUIC connection selects, drops, or stalls on a specific network path.**

The `iroh` crate from the `n0-computer/iroh` repository maintains a live view of every candidate path a QUIC connection can use. When packets stop flowing or a relay is selected unexpectedly, you need visibility into that routing state. Learning how to debug iroh connection issues using path events and connection state inspection gives you deterministic tools to observe `PathEvent` emissions and snapshot the active path list at any moment.

## The Two APIs: `path_events()` and `paths()`

`iroh` exposes two complementary debugging surfaces on its `Connection` object. Together they let you watch path history in real time and capture the current routing picture on demand.

- **`conn.path_events()`** returns an asynchronous `PathEventStream`. As implemented in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs), this stream yields `PathEvent` values whenever the path-selection subsystem adds a route, removes a route, or promotes a different route to active. Use it to watch NAT traversal, relay fallback, and link-speed changes as they happen.

- **`conn.paths()`** returns a `PathList` snapshot that implements `Iterator<Item = &PathInfo>`. Call it at any instant to collect per-path diagnostics such as `remote_addr()`, `rtt()`, `is_selected()`, and `is_relay()`. This call is cheap, non-blocking, and read-only.

## How Path State Is Managed Inside the `Connection` Struct

Inside [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) at line 447, the `Connection` struct stores a field named `paths: PathStateReceiver`. That receiver forwards updates from the path-selection subsystem to any listener attached via `path_events()`.

The `PathEvent` enum, defined around line 1017 in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs), carries three variants:

- `PathEvent::Added(PathInfo)` — a new candidate path was discovered. It may be direct, relay-based, or custom.
- `PathEvent::Removed(PathId)` — the path timed out or was discarded by the selector.
- `PathEvent::Selected(PathId)` — a different path was promoted to the active route for the connection.

When `Connection::paths()` is invoked, it materializes a `PathList` from the same underlying state. Each entry is a `PathInfo` that lets you correlate a `PathId` with concrete socket addresses and performance metrics. The default selector that drives these updates is `BiasedRttPathSelector`, located in [`iroh/src/socket/transports/biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/biased_rtt_path_selector.rs).

## Typical Troubleshooting Workflow for iroh Connections

Match your symptom to the event stream and snapshot data:

- **Connection stalls after handshake.** Check whether `PathEvent::Selected` fires after the initial path probe. If `conn.paths()` shows every route with an infinite RTT, the transport has not validated any candidate. Verify that at least one path reports a finite RTT before concluding the connection is dead.

- **Traffic flows only through a relay.** Watch for `PathEvent::Added` entries where `PathInfo::is_direct()` is true but never become selected. The `BiasedRttPathSelector` may filter them out if they are considered unreliable. Log each added path and its flags to confirm whether a direct candidate exists.

- **Path flaps between two IP addresses.** Repeated `PathEvent::Selected` variants with different IDs indicate oscillation. Compare the RTT values of the two paths in `conn.paths()`. If the RTTs are close, the selector may be bouncing between near-equal options. You can increase hysteresis or swap the selector to isolate the behavior.

- **No path events appear at all.** If the stream stays silent, the transport likely failed to discover any candidate. Confirm the underlying socket can send and receive UDP packets. Firewall rules that block STUN or QUIC traffic prevent the `PathStateReceiver` from ever emitting an event.

## Code Examples for Path Event Debugging

The following snippets show how to instrument an `iroh` client or server with path-level logging.

### Watch path events and log the selected path

```rust
let mut events = conn.path_events();

while let Some(event) = events.next().await {
    match event {
        PathEvent::Added(p) => {
            debug!("new path [{:?}] {}", p.id(), p.remote_addr());
        }
        PathEvent::Removed(id) => {
            debug!("path removed [{:?}]", id);
        }
        PathEvent::Selected(id) => {
            let selected = conn
                .paths()
                .iter()
                .find(|p| p.id() == id)
                .expect("selected path must exist");
            info!(
                "selected path switched to [{:?}] {} (rtt={:?})",
                selected.id(),
                selected.remote_addr(),
                selected.rtt()
            );
        }
    }
}

```

### One-off inspection of the current path list

```rust
let paths = conn.paths();
for p in paths.iter() {
    println!(
        "Path {} – {} – rtt={:?} – selected={} – relay={}",
        p.id(),
        p.remote_addr(),
        p.rtt(),
        p.is_selected(),
        p.is_relay()
    );
}

```

### Wait for a direct path selection

```rust
conn.wait_selected(Duration::from_secs(10), |p| p.remote_addr().is_ip())
    .await
    .expect("no direct path selected within timeout");

```

The `wait_selected` helper is available in the test utilities. You can reproduce the same logic by combining `path_events()` with a manual `paths()` check inside a timeout loop.

## Key Source Files for Deep Dives

For deeper inspection of the implementation, study these files in the `n0-computer/iroh` repository:

- [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) — Defines `Connection`, stores the `PathStateReceiver` at line 447, and implements `path_events()` and `paths()` between lines 447 and 452.
- [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs) — Contains the `PathEvent` enum and `PathInfo` structure around line 1017.
- [`iroh/src/socket/transports/biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/biased_rtt_path_selector.rs) — Implements the default selector that balances RTT against relay fallback.
- [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs) — Shows how relay paths are created and marked, which is critical when debugging unexpected relay selections.
- [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs) — Demonstrates concrete test logging of the selected path using `conn.path_events()` at lines 429–432.

## Summary

- **`conn.path_events()`** yields a live `PathEventStream` of added, removed, and selected path events for the full connection lifetime.
- **`conn.paths()`** provides a cheap, read-only `PathList` snapshot with per-route RTT, address, and selection status.
- The `Connection` struct holds path state in a `PathStateReceiver` field inside [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs).
- Use event streaming plus snapshot cross-referencing to diagnose stalls, relay fallback, and path oscillation deterministically.

## Frequently Asked Questions

### What is the difference between `path_events()` and `paths()` in iroh?

`path_events()` returns an asynchronous stream that emits `PathEvent` values every time the path state changes. `paths()` returns an immediate snapshot of the current `PathList`. You should use the stream for real-time tracing and the snapshot for point-in-time diagnostics.

### Why does my iroh connection only use a relay path?

If direct paths never receive a `PathEvent::Selected` promotion, the `BiasedRttPathSelector` may consider them unreliable or unreachable. Log every `PathEvent::Added` event and inspect `is_direct()` on the resulting `PathInfo`. If direct candidates exist but are ignored, check RTT and firewall conditions.

### How do I detect path flapping or oscillation in iroh?

Monitor `path_events()` for rapid sequences of `PathEvent::Selected` with different `PathId` values. When flapping occurs, call `conn.paths()` to compare RTTs. If two paths have nearly identical RTTs, the default selector may alternate between them.

### Where does iroh store path state inside the Connection object?

The `Connection` struct stores path updates in a `paths: PathStateReceiver` field at line 447 of [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs). That receiver bridges the path-selection subsystem and the public `path_events()` and `paths()` APIs.