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

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 (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 (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:

(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) 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:

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:

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.
  • 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 (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, which defines BiasedRttPathSelector and the TransportBias struct. The trait definition and address kinds are in iroh/src/socket/transports.rs. The integration point that connects selectors to connection state is iroh/src/socket/remote_map.rs, while iroh/src/socket/remote_map/remote_state.rs implements the PathSelectionContext and data structures the selector operates on.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →