How iroh Path Selection Works: Inside the Biased RTT Algorithm
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 (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. 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):
(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:
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:
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. When the actor needs to route data, it constructs a PathSelectionContext (implemented in 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. 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:
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:
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
BiasedRttPathSelectorby 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
RemoteStateActorviaRemoteMapand can be replaced at runtime by implementing thePathSelectortrait behind theunstable-custom-transportsfeature.
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. 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →