# How iroh's Path Selector Chooses Between Direct and Relay Connections

> Discover how iroh's Path Selector prioritizes direct or relay connections using biased RTT and transport tiers. Learn the default BiasedRttPathSelector logic.

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

---

**iroh uses a pluggable `PathSelector` interface with a default `BiasedRttPathSelector` implementation that sorts available network paths by transport tier and biased round-trip time to determine whether to use a direct IP connection or a relay.**

The path selection mechanism in the n0-computer/iroh repository governs how nodes negotiate the optimal network path for QUIC connections. By categorizing transports as either primary (direct IPv4/IPv6) or backup (relay) and applying deterministic RTT biases, the system ensures low-latency direct connections while maintaining reliable relay fallback.

## Transport Classification and Tier System

The `BiasedRttPathSelector` 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) categorizes every candidate path into a **transport tier** that determines its priority regardless of measured latency. Direct IP addresses are classified as **Primary** transports, while relay connections are classified as **Backup** transports.

The default bias map constructed in `BiasedRttPathSelector::default` (lines 96-103) assigns the following characteristics:

- **IPv4** (`AddrKind::IpV4`): Primary transport with no RTT bias
- **IPv6** (`AddrKind::IpV6`): Primary transport with a **3 ms advantage** (`IPV6_RTT_ADVANTAGE`) to prefer modern dual-stack endpoints
- **Relay** (`AddrKind::Relay`): Backup transport with no RTT bias, used only when no primary path exists

This tier system ensures that any available direct path automatically outranks a relay connection, even if the relay exhibits lower measured latency.

## Biased RTT Calculation and Sorting

For each candidate path, the selector computes a **sort key** using the `sort_key` method that combines the transport tier with a biased latency value:

```rust
fn sort_key(&self, addr: &FourTuple, rtt: Duration) -> (TransportType, i128) {
    let bias = self.bias_for(addr);
    let biased_rtt = (rtt.as_nanos() as i128).saturating_add(bias.rtt_bias);
    (bias.transport_type, biased_rtt)
}

```

The `bias.rtt_bias` value is **negative** when granting an advantage (e.g., IPv6 receives `-3ms` converted to nanoseconds) and **positive** when imposing a penalty. The resulting tuple `(TransportType, biased_rtt)` is ordered lexicographically, ensuring all primary transports sort before backup transports, with the lowest biased RTT breaking ties within each tier.

## Selection Logic and Path Switching

When evaluating available paths via `PathSelectionContext`, the `select` method (lines 70-83 in [`biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/biased_rtt_path_selector.rs)) implements hysteresis to prevent connection flapping:

1. **Cross-tier switching**: If the best candidate belongs to a different tier than the current path (e.g., moving from backup relay to primary direct), the selector **immediately switches** regardless of RTT values. This allows instant migration to direct paths as soon as they become available.

2. **Same-tier switching**: When both current and candidate paths share the same tier, the selector requires the new path to be at least **5 ms better** (`RTT_SWITCHING_MIN`) than the current one before switching. This threshold absorbs network jitter and prevents oscillation between paths of similar quality.

## Integration with Remote State Management

The `RemoteMap` struct in [`iroh/src/socket/remote_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map.rs) instantiates the path selector once during node initialization and stores it in `Tasks.path_selector`. Each `RemoteStateActor` receives a clone of this `Arc<dyn PathSelector>` and invokes the `select` method whenever path conditions change, such as new RTT measurements from `quinn` or discovered direct addresses via hole punching.

The chosen `FourTuple` determines whether subsequent QUIC packets route through a **direct IP address** or the **relay's endpoint**, with the selector re-evaluating this decision continuously as network conditions evolve.

## Customizing Path Selection Behavior

Developers can inject custom selectors or modify the default biases to alter path preferences.

### Using the Default Selector

The following pattern demonstrates how the default selector prioritizes direct connections over relay despite higher latency:

```rust
use iroh::socket::biased_rtt_path_selector::BiasedRttPathSelector;
use iroh::socket::remote_map::{PathSelectionContext, PathSelectionData};
use iroh::socket::transports::{self, Addr, FourTuple};
use std::time::Duration;

// Create a selector with the built‑in biases.
let selector = BiasedRttPathSelector::default();

// Mock two candidate paths: an IPv4 direct path and a relay backup.
let v4 = FourTuple::from_remote(Addr::Ip("127.0.0.1:1234".parse().unwrap()));
let relay = FourTuple::from_remote(Addr::Relay(
    "https://relay.iroh.computer".parse().unwrap(),
    iroh_base::EndpointId::from_bytes(&[0u8; 32]).unwrap(),
));

// Attach fake RTT measurements.
let paths = vec![
    PathSelectionData::for_test(&v4, Some(noq::PathStats { rtt: Duration::from_millis(30), ..Default::default() })),
    PathSelectionData::for_test(&relay, Some(noq::PathStats { rtt: Duration::from_millis(5), ..Default::default() })),
];

// No current path (first selection).
let ctx = PathSelectionContext::for_test(None, paths);
let chosen = selector.select(&ctx).selected().cloned();

assert_eq!(chosen.map(|p| p.remote()), Some(Addr::Ip(_))); // direct wins despite higher RTT

```

### Modifying Transport Biases

To adjust the preference hierarchy, override specific transport biases:

```rust
use iroh::socket::biased_rtt_path_selector::{
    BiasedRttPathSelector, TransportBias, TransportType,
};
use iroh::socket::transports::AddrKind;

// Start from the default selector.
let selector = BiasedRttPathSelector::default()
    // Give the relay a 2 ms advantage (negative bias → more preferred).
    .with_bias(
        AddrKind::Relay,
        TransportBias {
            transport_type: TransportType::Backup,
            rtt_bias: -(Duration::from_millis(2).as_nanos() as i128),
        },
    );

```

Custom selectors can be passed to `RemoteMap::new` to control path selection across the entire node runtime.

## Summary

- **iroh** implements path selection via a pluggable `PathSelector` trait, defaulting to `BiasedRttPathSelector`.
- **Primary transports** (direct IPv4/IPv6) always outrank **backup transports** (relay), with IPv6 receiving a 3 ms preference over IPv4.
- The selector uses **biased RTT calculations** where negative bias values indicate transport advantages.
- **Cross-tier switches** happen immediately when direct paths appear, while **same-tier switches** require a 5 ms RTT improvement to prevent flapping.
- The `RemoteMap` owns the selector and distributes it to `RemoteStateActor` instances, which re-evaluate paths on every RTT update.

## Frequently Asked Questions

### What happens when a direct connection becomes available while using a relay?

When a direct path is discovered, the selector recognizes a **cross-tier switch** from backup (relay) to primary (direct) and immediately migrates the connection, even if the direct path has a higher RTT than the relay. This behavior is hardcoded in `BiasedRttPathSelector::select` to prioritize direct connectivity.

### Why does IPv6 have a 3 ms advantage over IPv4?

The `IPV6_RTT_ADVANTAGE` constant in [`biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/biased_rtt_path_selector.rs) provides a slight preference for IPv6 endpoints to encourage the use of modern dual-stack infrastructure when latency is otherwise equivalent. This bias is applied as a negative offset during the RTT calculation phase.

### How does the selector prevent constant switching between paths?

The selector implements a **5 ms hysteresis threshold** (`RTT_SWITCHING_MIN`) for same-tier comparisons. A new path must demonstrate at least 5 ms lower biased RTT than the current path before the selector switches, absorbing micro-variations in network latency that would otherwise cause connection instability.

### Can I force iroh to use relay connections exclusively?

Yes, by implementing a custom `PathSelector` that assigns `TransportType::Backup` to direct addresses and `TransportType::Primary` to relay addresses (or by setting a large positive RTT bias on direct transports). You would then inject this custom selector into `RemoteMap::new` during node initialization to override the default `BiasedRttPathSelector` behavior.