# How Path Selection Works in iroh: The Biased RTT Algorithm Explained

> Understand iroh path selection. Learn how the Biased RTT algorithm prioritizes direct connections, uses lowest RTT, and prevents route flapping for optimal performance.

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

---

**iroh selects network paths using a `BiasedRttPathSelector` that prefers primary transports (direct IPv4/IPv6) over backup relays, chooses the path with the lowest biased RTT, and applies a 5 ms hysteresis threshold to prevent flapping between similar-quality routes.**

Path selection in iroh determines which network route carries your QUIC traffic at any given moment. The n0-computer/iroh repository abstracts every possible network route—IPv4, IPv6, relay, or custom transports—as a *path*, and uses a `PathSelector` trait to decide which active path to use. The default implementation, `BiasedRttPathSelector`, employs a deterministic tiering and latency-bias strategy to balance performance with stability.

## Transport Tiering and Address Classification

iroh categorizes every transport into two tiers: **Primary** and **Backup**. Primary transports include direct IPv4 and IPv6 connections, while backup transports (currently limited to the relay transport) are only selected when no primary path is available. This classification is encoded in the `TransportType` enum 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).

The tiering ensures that relay servers act as a fallback rather than a peer-of-first-resort. When the selector evaluates candidates, it treats the transport tier as the most significant component of the decision key, ensuring that a marginal primary path always wins over a perfect relay connection.

## RTT Bias Calculation and the Sorting Key

For each available path, the selector computes a **biased RTT** by adding a per-address-kind bias to the measured round-trip time. According to the `Default::default()` implementation 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 96-103), IPv6 addresses receive a small advantage (negative bias), relays receive no bias, and custom transports start with a neutral bias.

The selector then generates a two-part sorting key for ranking paths:

```rust
(transport_type, biased_rtt)

```

Where `biased_rtt = measured_rtt + bias.rtt_bias`. The `sort_key()` function (lines 129-133) produces this tuple, and paths are ordered such that lower keys represent better candidates. This design ensures that primary transports are always sorted above backup transports, and within the same tier, the path with the lowest effective latency wins.

## Flap Avoidance and Tier Crossing Logic

To prevent rapid oscillation between paths with similar performance characteristics, `BiasedRttPathSelector` implements a stickiness mechanism. When comparing a new candidate against the currently selected path within the same tier, the selector only switches if the new path’s biased RTT is at least **5 ms** lower than the current one. This threshold is defined by the constant `RTT_SWITCHING_MIN` and implemented in the `select()` method (lines 78-82).

However, stickiness does not apply when crossing tiers. If a primary path becomes available while the connection is using a backup relay, the selector switches immediately regardless of the RTT difference. This logic resides in the `if current_tier != best_tier` branch of `select()` (lines 76-79).

## Integration with Remote State Management

Every `RemoteStateActor` accesses the global path selector through an `Arc<dyn PathSelector>` stored in `RemoteMap::tasks.path_selector`. When the actor needs to send data, it constructs a `PathSelectionContext` (defined 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 invokes `selector.select(&ctx)`. The returned `PathSelection` struct indicates which concrete path to use for the next transmission.

The selector can be swapped at runtime via the `RemoteMap::new()` constructor, enabling A/B testing or emergency overrides without recompiling the crate.

## Implementing Custom Path Selectors

For advanced use cases, iroh exposes the `PathSelector` trait behind the `unstable-custom-transports` feature flag. You can implement a custom selector by defining a type that implements `select(&PathSelectionContext<'_>) -> PathSelection`.

Here is a complete example that preferentially selects any path with an RTT under 50 ms:

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

#[derive(Debug)]
struct LowLatencySelector;

impl PathSelector for LowLatencySelector {
    fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
        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()
    }
}

// Usage when constructing the RemoteMap
let selector = Arc::new(LowLatencySelector);
let remote_map = RemoteMap::new(
    metrics,
    local_direct_addrs,
    address_lookup,
    shutdown_token,
    selector,
    span,
);

```

To use the default selector with custom biases—for example, strongly preferring IPv6—you can configure `BiasedRttPathSelector` before passing it to `RemoteMap`:

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

let mut selector = BiasedRttPathSelector::default();
selector.set_bias(
    AddrKind::IpV6,
    TransportBias::primary().with_rtt_advantage(std::time::Duration::from_millis(10)),
);

let remote_map = RemoteMap::new(
    metrics,
    local_direct_addrs,
    address_lookup,
    shutdown_token,
    Arc::new(selector),
    span,
);

```

## Summary

- **Path selection in iroh** is handled by the `PathSelector` trait, with `BiasedRttPathSelector` as the default implementation.
- **Transport tiering** splits paths into Primary (direct IPv4/IPv6) and Backup (relay) categories, with primary always preferred.
- **Biased RTT** combines measured latency with address-kind biases, computed in `sort_key()` at lines 129-133 of [`biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/biased_rtt_path_selector.rs).
- **Flap avoidance** requires a 5 ms improvement (`RTT_SWITCHING_MIN`) before switching paths within the same tier.
- **Tier crossing** is immediate: if a primary path appears while using a backup, the selector switches instantly.
- **Customization** is supported via the `unstable-custom-transports` feature, allowing you to inject an `Arc<dyn PathSelector>` into `RemoteMap`.

## Frequently Asked Questions

### What is the default path selection algorithm in iroh?

The default algorithm is `BiasedRttPathSelector`, which prioritizes primary transports (direct IPv4/IPv6) over backup relays, then selects the path with the lowest biased RTT. It adds a configurable bias to measured RTT based on address kind—for example, giving IPv6 a slight advantage—and applies a 5 ms hysteresis threshold to prevent flapping.

### How does iroh prevent path flapping between similar routes?

The selector implements a stickiness mechanism that prevents switching to a new path unless its biased RTT is at least 5 ms lower than the current path’s biased RTT. This threshold, defined as `RTT_SWITCHING_MIN` 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 78-82), ensures that minor RTT jitter does not trigger constant path changes.

### Can I implement a custom path selector for specialized network conditions?

Yes, by enabling the `unstable-custom-transports` feature, you can implement the `PathSelector` trait and pass an `Arc<dyn PathSelector>` to the `RemoteMap` constructor. Your implementation receives a `PathSelectionContext` containing path statistics and must return a `PathSelection` indicating which path to use. This allows you to create policies based on bandwidth, geographic location, or custom metrics rather than RTT alone.

### Where does the path selection logic reside in the iroh codebase?

The core selection algorithm lives 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), which defines `BiasedRttPathSelector` and the `TransportBias` struct. The trait definition and address kinds are in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs). The integration point that connects selectors to connection state is [`iroh/src/socket/remote_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map.rs), while [`iroh/src/socket/remote_map/remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map/remote_state.rs) implements the `PathSelectionContext` and data structures the selector operates on.