# Iroh Path Selection and Network Transition Handling: How BiasedRttPathSelector Works

> Explore Iroh path selection and network transition handling. Learn how BiasedRttPathSelector uses a tier-based algorithm with RTT biases for efficient direct IP path preference and reduced flapping.

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

---

**Iroh's path selection uses a deterministic tier-based algorithm that prefers direct IP paths over relays with configurable RTT biases and a 5ms stickiness threshold to prevent flapping.**

The `n0-computer/iroh` networking library implements intelligent path selection to maintain stable QUIC connections across changing network conditions. At the core of this system is the **`BiasedRttPathSelector`**, which balances latency, reliability, and transport tiering to choose between direct IPv4/IPv6 paths and relay connections. This article examines the implementation details found in [`iroh/src/socket/biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/biased_rtt_path_selector.rs) and explains how Iroh handles network transitions without configuration.

## How Path Selection Works in Iroh

Iroh encapsulates all path-selection logic behind the **`PathSelector`** trait, with `BiasedRttPathSelector` serving as the default implementation. The selector evaluates candidate paths every time the socket layer updates its remote path set, ensuring optimal connectivity without manual intervention.

### Transport Tiering: Primary vs. Backup

The selector classifies every transport into one of two tiers using the **`TransportType`** enum:

```rust
enum TransportType {
    Primary,   // Direct IP paths (IPv4/IPv6)
    Backup,    // Relays
}

```

**Primary** transports represent direct IP paths and are always considered before backup options. **Backup** transports—currently limited to relay connections—only come into consideration when no primary path is available. This tiering ensures that even if a relay offers lower latency, direct paths remain preferred for stability and performance.

### RTT Biasing and Sort Keys

To compare paths fairly across different address types, the selector applies configurable biases through the **`TransportBias`** struct:

```rust
pub(crate) struct TransportBias {
    transport_type: TransportType,
    rtt_bias: i128,               // nanoseconds; negative = advantage
}

```

Default biases are stored in a `FxHashMap<AddrKind, TransportBias>` within the selector:

- **`IpV4`**: Primary tier, 0ns bias
- **`IpV6`**: Primary tier, **-3ms** advantage (favoring IPv6 when RTTs are equal)
- **`Relay`**: Backup tier, 0ns bias

For each candidate path, the selector computes a **sort key** tuple:

```

( transport_type , biased_rtt )

```

Where `biased_rtt = measured_rtt + bias.rtt_bias`. The selector sorts candidates by this tuple, preferring lower values first by tier, then by latency.

### The 5ms Stickiness Threshold

To prevent path flapping caused by RTT jitter, the implementation enforces a minimum improvement threshold before switching paths within the same tier:

```rust
const RTT_SWITCHING_MIN: Duration = Duration::from_millis(5);

```

A path switch only occurs when the new candidate's biased RTT is **at least 5ms better** than the current path's biased RTT. This constant is defined in [`biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/biased_rtt_path_selector.rs) and eliminates oscillation between paths with similar performance characteristics.

### Selection Algorithm Implementation

The **`select`** method (lines 36-84 in [`biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/biased_rtt_path_selector.rs)) iterates over all candidates in the `PathSelectionContext` and implements the following decision logic:

1. **No current path** → Select the best candidate immediately
2. **Tier change** (`Primary` ↔ `Backup`) → Switch immediately regardless of RTT difference
3. **Same tier** → Switch only if the candidate beats the current path by `RTT_SWITCHING_MIN` (5ms)

If none of these conditions trigger, the selector returns **`PathSelection::none()`**, instructing the connection manager to maintain the current path. This deterministic approach ensures predictable behavior across all network events.

## Network Transition Handling

Network transitions—whether from mobility, interface changes, or connectivity loss—are handled entirely within the selector's evaluation cycle. Because the selector runs every time the socket layer discovers a new path, closes an existing path, or updates RTT statistics, it can react immediately to changing conditions.

### Handling Common Network Events

| Event | Selector Behavior |
|-------|------------------|
| **New direct path appears** | If primary tier and biased RTT beats current by ≥5ms, or no current primary exists, switch immediately |
| **Current path disappears** | No current key exists → selector immediately picks the best remaining candidate |
| **RTT degradation** | Once degradation crosses the 5ms threshold relative to alternatives, triggers automatic switch |
| **Relay fallback** | When no primary paths remain reachable, automatically selects backup tier (relay) even with higher RTT |

The connection manager in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) only performs a transition when the selector explicitly returns a `PathSelection` with a new path. When the selector returns `none()`, the existing connection continues uninterrupted.

## Practical Implementation Examples

### Using the Default Selector

The default selector requires no configuration and works automatically within Iroh's socket layer:

```rust
use iroh::socket::remote_map::{PathSelectionContext, PathSelection};
use iroh::socket::BiasedRttPathSelector;

// ctx is provided by the socket layer during path evaluation
let ctx: PathSelectionContext = /* ... */;

let selection: PathSelection = BiasedRttPathSelector::default()
    .select(&ctx);

if let Some(new_path) = selection.selected() {
    // Apply the new FourTuple endpoint to the QUIC connection
}

```

### Customizing Transport Biases

For specialized network environments, you can adjust biases using the **`unstable-custom-transports`** feature:

```rust
use std::time::Duration;
use iroh::socket::transports::AddrKind;
use iroh::socket::biased_rtt_path_selector::{BiasedRttPathSelector, TransportBias};

let selector = BiasedRttPathSelector::default()
    .with_bias(
        AddrKind::IpV6,
        TransportBias::primary().with_rtt_advantage(Duration::from_millis(10)),
    );

```

This configuration gives IPv6 a 10ms advantage over IPv4 when selecting paths. Note that `with_bias` is only available with the `unstable-custom-transports` feature flag; production deployments typically rely on the default biases defined in the source.

### Debugging Path Selection

To inspect how the selector ranks specific paths, use the **`sort_key`** method:

```rust
use iroh::socket::transports::FourTuple;

let addr: FourTuple = /* remote address */;
let measured_rtt = Duration::from_millis(30);

let (tier, biased_rtt) = selector.sort_key(&addr, measured_rtt);
println!("Tier: {:?}, Biased RTT: {}ns", tier, biased_rtt);

```

This reveals the exact tuple used for comparison, helping diagnose why the selector prefers one path over another.

## Summary

Iroh's path selection and network transition handling rely on a **deterministic, bias-driven algorithm** that:

- Separates transports into **primary** (direct IP) and **backup** (relay) tiers
- Applies configurable RTT biases per address type via `TransportBias`
- Computes sort keys as `(tier, biased_rtt)` tuples for comparison
- Enforces a **5ms stickiness threshold** (`RTT_SWITCHING_MIN`) to prevent flapping
- Automatically falls back to relay transports when direct paths become unavailable
- Returns `PathSelection::none()` to maintain current paths when no improvement meets the threshold

All logic resides in [`iroh/src/socket/biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/biased_rtt_path_selector.rs) and integrates with the broader socket management system in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs).

## Frequently Asked Questions

### How does Iroh decide between IPv4 and IPv6 paths?

According to the `BiasedRttPathSelector` implementation in [`biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/biased_rtt_path_selector.rs), IPv6 receives a default **-3ms bias** advantage over IPv4. When measured RTTs are equal, IPv6 wins by 3ms. If IPv4 actually measures more than 3ms faster, it becomes the preferred path, provided the difference exceeds the 5ms stickiness threshold.

### Why does Iroh stay on a relay even when direct connections are available?

Iroh will not switch from a relay (backup tier) to a direct path (primary tier) based solely on latency measurements. **Tier changes trigger immediate switches**, so if a direct path becomes available, the selector immediately promotes it to primary regardless of RTT. If the selector is not switching, the direct path may not be fully established or the `PathSelectionContext` hasn't yet included it in the candidate set.

### What happens when the current network path suddenly drops?

When a path disappears, the `PathSelectionContext` no longer contains a valid key for the current address. The selector detects this absence and immediately selects the best remaining candidate from the available set, whether that's another direct path or a fallback to the relay tier. This transition happens during the next evaluation cycle without application intervention.

### Can I adjust the 5ms threshold for path switching?

The `RTT_SWITCHING_MIN` constant is hardcoded at 5ms in the source code. To modify this behavior, you would need to implement a custom `PathSelector` trait or modify the `BiasedRttPathSelector` implementation. The 5ms value represents a balance between responsiveness to genuine network improvements and resistance to measurement jitter.