# How Iroh Handles Connection Failures and Automatically Falls Back to Relays

> Learn how Iroh handles connection failures using dual transport layers. Discover its automatic failover to relays, ensuring your application stays connected seamlessly.

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

---

**Iroh maintains two independent transport layers—a direct QUIC path and a persistent relay connection—to ensure seamless failover when direct peer-to-peer connections fail, automatically switching traffic to relays without application intervention.**

Iroh, the open-source peer-to-peer networking library from n0-computer, implements sophisticated connection failure handling that keeps communications alive even when NAT traversal fails or network links drop. By architecting every peer connection with dual transport paths, the library ensures that applications remain connected through automatic relay fallback when direct UDP hole-punching becomes impossible.

## The Dual Transport Architecture

Every connection in iroh is built on top of **two independent transport layers** that operate simultaneously:

- **Direct QUIC path**: Attempts to establish peer-to-peer UDP connections after NAT traversal, implemented in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs). The `connect` handshake creates a `noq::Connecting` and later registers the resulting `noq::Connection` via `conn_from_noq_conn`.

- **Relay path**: Maintains a persistent QUIC connection to a relay server that can forward datagrams when direct communication fails, managed in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs). The `RelayActor` owns one or more `ActiveRelayActor` instances, each maintaining an `iroh_relay::client::Client` to a specific `RelayUrl`.

When an endpoint is constructed, it receives a **`RelayMode`** (Default, Staging, Custom, or Disabled) that determines which relay servers to use. In [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) (lines 153-167), this mode converts into a `TransportConfig`:

```rust
// iroh/src/endpoint.rs – TransportConfig conversion
impl From<RelayMode> for Option<TransportConfig> {
    fn from(mode: RelayMode) -> Self {
        match mode {
            RelayMode::Disabled => None,
            RelayMode::Default => Some(TransportConfig::Relay {
                urls: crate::defaults::prod::default_relay_map(),
            }),
            RelayMode::Staging => Some(TransportConfig::Relay {
                urls: crate::defaults::staging::default_relay_map(),
            }),
            RelayMode::Custom(map) => Some(TransportConfig::Relay { urls: map.clone() }),
        }
    }
}

```

## Address Resolution and Connection Ordering

When `Endpoint::connect` is invoked, the endpoint builds an **`EndpointAddr`** containing both direct IP addresses (from the address-lookup service) and relay addresses (`TransportAddr::Relay(url)`).

The connection logic in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) (around line 1510) orders these addresses with **direct transports prioritized over relays**:

```rust
// iroh/src/endpoint/connection.rs – address ordering
let mut addrs = ep_addr.into_iter().collect::<Vec<_>>();
// Custom (direct) transports are primary, relay transports are backup

```

The `Connecting` future attempts each address sequentially. If the direct path fails to establish a QUIC handshake, the implementation automatically proceeds to the next address in the list. Because the relay address is always present (unless `RelayMode::Disabled` is configured), the connection seamlessly falls back to the relay without requiring additional user code or error handling.

## The Relay Actor: Keeping Fallback Paths Alive

Even while attempting direct connections, the **relay path remains active** through the `RelayActor` system. When the endpoint initializes, `RelayActor::new` launches an `ActiveRelayActor` for each configured relay URL.

The `ActiveRelayActor::run_once` method executes a continuous loop that:

1. **Dials** the relay server with exponential back-off if the initial connection fails
2. **Maintains** the QUIC session with keep-alive traffic
3. **Re-sends** any datagrams that could not be delivered during connection interruptions (see `run_once` lines 363-373)
4. **Updates** the shared `RelayStatus` via `set_status(&self.url, RelayConnectionState::Connected/Disconnected)` to inform the socket layer of availability

If the relay connection drops, the `ActiveRelayActor` automatically restarts it, guaranteeing that a fallback path is always ready when needed.

## Detecting Failures and Switching Paths

The socket layer maintains a **`PathList`** per connection accessible via `Connection::paths()`. This structure tracks both the direct QUIC path and the relay path simultaneously.

Applications can monitor transport changes through the `PathEvent` stream obtained from `Connection::path_events()`. This notifies callers when:

- A new direct path becomes **selected** (hole-punching succeeded)
- The selected path falls back to the **relay** (direct path timeout or link failure)
- A path is **closed** (permanent failure)

When the direct path reports a `ConnectionError` or the underlying socket detects loss of the selected path, the `PathList` removes the failing entry. The relay path remains active in the list, ensuring the connection stays usable while the system attempts recovery.

## Real-World Recovery: Link Outage Test

The automatic failover behavior is validated in [`iroh/tests/patchbay.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay.rs), which simulates network infrastructure failures. In the link-outage test (lines 188-191), the endpoint first establishes a hole-punched direct connection, then the physical interface is taken down:

```rust
// iroh/tests/patchbay.rs – link-outage recovery (excerpt)
dev.iface("eth0").unwrap().link_down().await?;
tokio::time::sleep(downtime).await;
dev.iface("eth0").unwrap().link_up().await?;   // link restored
ping_open(&conn, timeout).await?;             // works via relay while link is down
conn.wait_ip(timeout).await?;                  // waits for a new direct path after link up

```

During the outage, `ping_open` continues to succeed via the relay. When the link returns, `conn.wait_ip` detects the new direct path, and the endpoint automatically switches back to peer-to-peer communication while maintaining the relay as a backup for future failures.

## Configuring Relay Modes

Developers control relay behavior through the `RelayMode` enum:

- **`RelayMode::Default`**: Uses the production relay map for maximum connectivity
- **`RelayMode::Staging`**: Connects to staging relay servers for testing
- **`RelayMode::Custom(map)`**: Specifies custom relay URLs for private infrastructure
- **`RelayMode::Disabled`**: Removes relay fallback entirely (connections fail if direct paths unavailable)

## Practical Implementation

The following example demonstrates building an endpoint with automatic relay fallback:

```rust
use iroh::endpoint::{Endpoint, RelayMode};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Build an endpoint that uses the production relay map by default.
    let ep = Endpoint::builder()
        .relay_mode(RelayMode::Default) // <-- adds a persistent relay path
        .bind()
        .await?;

    // `addr` may contain both direct IPs (from the address-lookup) and a relay URL.
    let conn = ep.connect(addr, b"my/alpn").await?; // tries direct first, falls back to relay

    // The connection stays alive even if the direct path drops.
    // You can observe the current path via `conn.paths()` or listen for changes:
    let mut events = conn.path_events();
    while let Some(event) = events.next().await {
        println!("Path event: {event:?}");
    }

    Ok(())
}

```

In this implementation, the `RelayActor` continuously maintains the relay connection in the background. If direct establishment fails due to symmetric NATs or firewall rules, the connection succeeds via relay and automatically migrates to direct communication once hole-punching becomes possible.

## Summary

- **Iroh creates parallel transport layers** for every connection, maintaining both direct QUIC paths and persistent relay connections simultaneously.
- **Address ordering prioritizes direct communication** but includes relay URLs as automatic fallback options when `connect` iterates through available transports.
- **The RelayActor system** manages relay connections with exponential back-off, automatic reconnection, and status broadcasting to ensure fallback paths are always available.
- **PathList and PathEvent APIs** provide real-time visibility into transport state changes without requiring manual connection management.
- **Failed direct paths trigger automatic relay fallback** until NAT traversal succeeds again, at which point the connection seamlessly migrates back to the direct path.

## Frequently Asked Questions

### How does iroh decide when to switch from direct to relay connections?

Iroh attempts addresses in strict order—direct IPs first, then relay URLs. If the QUIC handshake fails on a direct address or the established direct path reports a `ConnectionError`, the `Connecting` future automatically tries the next address in the list. Once connected, the `PathList` monitors path health; if the selected direct path times out or drops, traffic automatically flows through the relay path that has been kept alive in parallel by the `ActiveRelayActor`.

### What happens if the relay server itself becomes unavailable?

Each `ActiveRelayActor` implements exponential back-off retry logic within its `run_once` loop. If a relay connection drops, the actor continuously attempts to reconnect while updating the shared `RelayStatus` to `Disconnected`. If all configured relays fail, the connection relies solely on direct paths, or fails entirely if no direct path exists and `RelayMode` is not disabled. Multiple relay URLs can be configured to improve redundancy.

### Can I disable relay fallback for security or compliance reasons?

Yes. Configure the endpoint with `RelayMode::Disabled` when building the connection. This prevents the inclusion of relay addresses in the `EndpointAddr` and stops the `RelayActor` from initializing, ensuring all traffic flows exclusively through direct peer-to-peer connections. Note that this may reduce connectivity success rates in restrictive network environments.

### How can I monitor which transport path is currently active?

Use the `Connection::path_events()` method to obtain a stream of `PathEvent` notifications that report when paths are selected, changed, or closed. Additionally, `Connection::paths()` returns the current `PathList` containing both active and standby paths. This allows applications to observe transport layer changes—such as failover to relay or recovery to direct paths—in real-time without polling.