Iroh Path Selection and Network Transition Handling: How BiasedRttPathSelector Works
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 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:
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:
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 biasIpV6: 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:
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 and eliminates oscillation between paths with similar performance characteristics.
Selection Algorithm Implementation
The select method (lines 36-84 in biased_rtt_path_selector.rs) iterates over all candidates in the PathSelectionContext and implements the following decision logic:
- No current path → Select the best candidate immediately
- Tier change (
Primary↔Backup) → Switch immediately regardless of RTT difference - 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 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:
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:
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:
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 and integrates with the broader socket management system in 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, 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.
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 →