# How iroh Path Selection Works: Inside the Biased RTT Algorithm

> Discover how iroh path selection uses BiasedRttPathSelector to prioritize direct connections over relays, ranking by latency and favoring IPv6 for optimal performance.

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

---

**iroh selects network paths using a `BiasedRttPathSelector` that prioritizes primary transports (direct IPv4/IPv6) over backup relays, ranks candidates by latency-adjusted RTT measurements where IPv6 receives a slight preference, and applies a 5 ms hysteresis threshold to prevent path flapping.**

The iroh networking stack treats every available network route—whether IPv4, IPv6, relay, or custom transport—as a distinct *path*. At runtime, the `PathSelector` trait determines which path should carry QUIC connections and data traffic. According to the n0-computer/iroh source code, the default `BiasedRttPathSelector` implements a deterministic, latency-biased policy that balances performance with connection stability.

## Transport Tiering: Primary vs. Backup

The selector first categorizes transports into two tiers defined 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) (lines 31-36). **Primary** transports include direct IPv4 and IPv6 connections. **Backup** transports consist solely of relay paths. The selector will only use a backup transport when no primary path is available.

This classification is encoded in the `TransportType` enum. When evaluating candidates, the selector compares tiers before comparing latencies, ensuring that a working direct connection is always preferred over a relay, regardless of raw RTT measurements.

## RTT Biasing by Address Kind

Within the same tier, iroh compares paths using a **biased RTT** rather than raw latency. The default implementation adds a per-address-kind bias (in nanoseconds) to every measured RTT:

- **IPv6** receives a small advantage (negative bias) to encourage modern address family usage
- **Relays** receive no advantage (neutral bias)
- **Custom transports** start with neutral bias

The bias table is constructed in the `Default::default()` implementation at lines 96-103 of [`biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/biased_rtt_path_selector.rs). This biasing allows operators to steer traffic toward preferred network paths without hardcoding priority lists.

## The Sorting Key and Selection Logic

For each candidate path, the selector computes a two-part sorting key in the `sort_key()` method (lines 129-133):

```rust
(transport_type, biased_rtt)

```

Where `biased_rtt = measured_rtt + bias.rtt_bias`. Lower keys are better. The tuple ordering ensures that primary transports always sort before backup transports, and within the same tier, the lowest biased RTT wins.

## Flap Avoidance and Stickiness

To prevent rapid oscillation when network conditions jitter, the selector implements **stickiness** with a 5 ms hysteresis threshold defined as `RTT_SWITCHING_MIN`. When evaluating whether to switch from the current path to a better candidate within the same tier, the selector requires the new path to have a biased RTT at least 5 ms lower than the current best.

This logic resides in the `select()` method at lines 78-82 of [`biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/biased_rtt_path_selector.rs):

```rust
else if best_biased + RTT_SWITCHING_MIN < current_biased {
    // Switch to the new path
}

```

## Immediate Tier Crossing

While the selector resists switching within the same tier for marginal gains, it immediately switches tiers when availability changes. If a primary path becomes available while the connection is using a backup relay (or vice versa), the selector switches immediately without waiting for the RTT margin.

This tier-crossing logic appears in the `select()` method at lines 76-79:

```rust
if current_tier != best_tier {
    // Always switch to the better tier
}

```

## Integration with Remote State Actors

Every `RemoteStateActor` accesses the global path selector through `RemoteMap::tasks.path_selector`, defined in [`iroh/src/socket/remote_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map.rs). When the actor needs to route data, it constructs a `PathSelectionContext` (implemented in [`iroh/src/socket/remote_map/remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map/remote_state.rs)) and calls `selector.select(&ctx)`.

The returned `PathSelection` structure contains the concrete `Path` to use for the next transmission. Developers can swap selectors at runtime by passing an `Arc<dyn PathSelector>` to the `RemoteMap::new()` constructor, enabling A/B testing or emergency failover policies without restarting the node.

## Implementing Custom Path Selectors

Behind the `unstable-custom-transports` feature flag, iroh exposes the `PathSelector` trait in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs). You can implement custom logic by providing a type that implements `select(&PathSelectionContext) -> PathSelection`.

### Customizing Biases

To adjust the default biases without writing a full selector:

```rust
use std::sync::Arc;
use std::time::Duration;
use iroh::socket::remote_map::RemoteMap;
use iroh::socket::biased_rtt_path_selector::BiasedRttPathSelector;
use iroh::socket::transports::AddrKind;
use iroh::socket::biased_rtt_path_selector::TransportBias;

// Create a selector that strongly prefers IPv6 by giving it a 10ms RTT advantage
let custom_selector = Arc::new(
    BiasedRttPathSelector::default()
        .with_bias(AddrKind::IpV6, TransportBias::primary().with_rtt_advantage(
            Duration::from_millis(10),
        )),
);

// Use it when constructing the RemoteMap
let remote_map = RemoteMap::new(
    metrics,
    local_direct_addrs,
    address_lookup,
    shutdown_token,
    custom_selector,
    span,
);

```

### Complete Custom Implementation

For entirely different policies, implement the trait directly:

```rust
use iroh::socket::remote_map::{PathSelector, PathSelection, PathSelectionContext};
use std::sync::Arc;

#[derive(Debug)]
struct LatencyThresholdSelector;

impl PathSelector for LatencyThresholdSelector {
    fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
        // Select first path with RTT under 50ms
        for psd in ctx.paths() {
            if let Some(stats) = psd.stats() {
                if stats.rtt < std::time::Duration::from_millis(50) {
                    let mut sel = PathSelection::none();
                    sel.set(&psd);
                    return sel;
                }
            }
        }
        PathSelection::none()
    }
}

// Install the custom selector
let selector = Arc::new(LatencyThresholdSelector);
let remote_map = RemoteMap::new(..., selector, ...);

```

## Summary

- **iroh path selection** uses the `BiasedRttPathSelector` by default, which abstracts every network route as a path and selects the optimal one based on transport tier and adjusted latency.
- **Primary transports** (direct IPv4/IPv6) are always preferred over **backup transports** (relays), with immediate switching when tier availability changes.
- Paths are ranked by a **biased RTT** value (measured RTT plus address-kind bias), giving IPv6 a slight advantage over IPv4.
- A **5 ms hysteresis threshold** (`RTT_SWITCHING_MIN`) prevents flapping between similar-quality paths within the same tier.
- The selector integrates with `RemoteStateActor` via `RemoteMap` and can be replaced at runtime by implementing the `PathSelector` trait behind the `unstable-custom-transports` feature.

## Frequently Asked Questions

### What algorithm does iroh use for path selection?

iroh uses the `BiasedRttPathSelector`, which categorizes paths into Primary (direct) and Backup (relay) tiers, then selects the path with the lowest biased RTT within the best available tier. The algorithm adds configurable latency biases per address kind and enforces a 5 ms stickiness threshold to prevent oscillation.

### How does iroh prevent connection flapping between paths?

The selector implements flap avoidance by requiring a new candidate to have a biased RTT at least 5 ms lower than the current path before switching, as defined by the `RTT_SWITCHING_MIN` constant in [`biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/biased_rtt_path_selector.rs). This hysteresis prevents rapid switching when network latency fluctuates.

### Can I replace the default path selector in iroh?

Yes. You can provide a custom `Arc<dyn PathSelector>` to `RemoteMap::new()` to replace the default biased RTT logic. To implement a custom selector, enable the `unstable-custom-transports` feature and implement the `PathSelector` trait with your own `select(&PathSelectionContext) -> PathSelection` method.

### Why does iroh give IPv6 addresses a bias advantage?

The default bias configuration in `BiasedRttPathSelector::default()` (lines 96-103) assigns a small negative bias to IPv6 to encourage the use of modern address families, while keeping relay and custom transport biases neutral. This steers traffic toward direct IPv6 connections when available without breaking IPv4 fallback.