# Handling iroh Connection Errors and Timeouts: Relay Actor Resilience Patterns

> Learn how Iroh handles connection errors and timeouts using its relay actor. Discover retry logic for dialing, handshake, and established connections.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-06-21

---

**Iroh handles connection errors and timeouts through a dedicated relay actor that classifies failures into distinct phases—dialing, handshake, and established connection—each with specific timeout constants and exponential backoff retry logic.**

The n0-computer/iroh repository implements robust error handling for relay connections through a specialized actor model in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs). Understanding how to handle iroh connection errors and timeouts is essential for building resilient peer-to-peer applications that gracefully recover from network instability.

## Understanding iroh Relay Connection Error Types

The relay actor models failures explicitly using three distinct error enums that correspond to different phases of the connection lifecycle.

### RelayConnectionError: The Top-Level Classification

`RelayConnectionError` serves as the primary error type returned from the actor's run loop. Defined in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs) at lines 216-224, this enum distinguishes between three critical failure phases:

- **Dial**: Failed to establish a TCP/QUIC connection
- **Handshake**: Connected but failed to complete the relay handshake
- **Established**: Connection dropped after successful establishment

### DialError: Connection Establishment Failures

`DialError` captures failures that occur while attempting to open a TCP/QUIC connection. Located at lines 252-256 in [`actor.rs`](https://github.com/n0-computer/iroh/blob/main/actor.rs), this variant includes a dedicated `Timeout` variant governed by the `CONNECT_TIMEOUT` constant and wraps low-level `ConnectError` types for DNS resolution or transport failures.

### RunError: Post-Establishment Failures

After a connection is established, `RunError` (lines 227-240 in [`actor.rs`](https://github.com/n0-computer/iroh/blob/main/actor.rs)) handles runtime failures including send timeouts, ping timeouts, and stream closure events. These errors wrap around the `Established` variant of `RelayConnectionError` when they cause connection termination.

## Timeout Constants and Their Applications

Iroh applies specific timeouts at distinct phases of the relay connection lifecycle to prevent indefinite blocking.

### CONNECT_TIMEOUT for the Dial Phase

The **dial timeout** limits the entire dial-and-handshake phase, covering DNS resolution, TCP/QUIC connection, and TLS handshake. In `dial_relay()` (lines 80-85), iroh uses `tokio::time::timeout(CONNECT_TIMEOUT, …)` to enforce this limit. The default value is typically 10 seconds, though the exact duration depends on the `RelayConnectionOptions` configuration.

### PING_INTERVAL for Keepalive and Send Operations

The **ping timeout** determines connection health after establishment. If a ping is sent without a corresponding pong within this interval, the connection is considered dead. This same interval serves as the **send timeout** in `run_sending()` (lines 42-44), limiting how long a batch of outbound datagrams may take to transmit.

### RELAY_INACTIVE_CLEANUP_TIME for Resource Management

To prevent resource leaks, iroh implements an **inactive relay cleanup** timeout defined at lines 64-66 in [`actor.rs`](https://github.com/n0-computer/iroh/blob/main/actor.rs). If no datagrams are sent for a configurable period, the actor shuts down the connection, except for the home relay which remains persistent.

## The Exponential Backoff Retry Loop

When errors occur, the outer `run()` loop implements sophisticated retry logic with exponential backoff to handle transient failures without overwhelming the network.

The backoff strategy is constructed via `build_backoff()` (lines 50-57) with the following characteristics:
- Minimum delay: 10 milliseconds
- Maximum delay: 16 seconds
- Jitter: Enabled to prevent thundering herds
- Retry limit: Unlimited

```rust
let mut backoff = Self::build_backoff();               // Creates exponential backoff
while let Err(err) = self.run_once().await {           // Returns RelayConnectionError
    warn!("{err:#}");
    let was_established = matches!(err, RelayConnectionError::Established { .. });
    
    if !was_established {
        // Dialing or handshake failed → wait before retrying
        if let Some(delay) = backoff.next() {
            debug!("retry in {delay:?}");
            time::sleep(delay).await;
        }
    } else {
        // Connection had been established → reset backoff and reconnect immediately
        backoff = Self::build_backoff();
    }
}

```

This logic distinguishes between initial connection failures (which back off) and dropped established connections (which reset the backoff and reconnect immediately).

## Detecting Errors in Application Code

When using the public `RelayActor` API, failures propagate as `anyhow::Error` values that can be downcast to specific `RelayConnectionError` variants for programmatic handling.

```rust
use iroh::relay::RelayActor;
use iroh::socket::transports::relay::RelayConnectionError;

#[tokio::main]
async fn main() {
    // Build a RelayActor (details omitted for brevity)
    let (actor, handle) = RelayActor::new(/* … */);
    let mut actor_fut = tokio::spawn(actor.run(/* … */));

    // Wait for the actor to finish (e.g. on shutdown)
    if let Err(err) = actor_fut.await.unwrap() {
        // Match on the concrete error kind
        if let Some(conn_err) = err.downcast_ref::<RelayConnectionError>() {
            match conn_err {
                RelayConnectionError::Dial { source } => {
                    eprintln!("Dial failed: {}", source);
                }
                RelayConnectionError::Handshake { source } => {
                    eprintln!("Handshake timed out or failed: {}", source);
                }
                RelayConnectionError::Established { source } => {
                    eprintln!("Connection lost after being up: {}", source);
                }
            }
        } else {
            eprintln!("Unexpected error: {}", err);
        }
    }
}

```

This pattern allows applications to distinguish between temporary network unavailability (dial failures) and persistent configuration issues (handshake failures).

## Customizing Timeouts for Network Conditions

When default values don't suit your network environment, you can adjust timeouts before creating a relay connection. Note that these constants are defined in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs) and may require custom configuration through the `ClientBuilder` or `RelayConnectionOptions`.

```rust
use iroh::relay::client::ClientBuilder;
use iroh::base::RelayUrl;
use std::time::Duration;

async fn custom_dial(url: RelayUrl) -> Result<(), anyhow::Error> {
    // Build a client with a longer dial timeout (30 s)
    let builder = ClientBuilder::new(url, /* secret_key */ Default::default(), /* dns */ Default::default())
        .tls_client_config(/* ... */)
        .address_family_selector(|| false); // IPv4 only for this example

    // Manually apply a timeout larger than the default CONNECT_TIMEOUT
    let client = tokio::time::timeout(Duration::from_secs(30), builder.connect())
        .await?
        .map_err(|e| anyhow::anyhow!("connect error: {}", e))?;

    // Use the client (ping/pong) – any ping timeout will be reported as RunError::PingTimeout
    let ping_tracker = iroh_relay::PingTracker::default();
    // … send a ping, await pong, handle errors …
    Ok(())
}

```

For production deployments on high-latency links, consider increasing `PING_INTERVAL` to 60 seconds and `RELAY_INACTIVE_CLEANUP_TIME` to 300 seconds to prevent premature connection teardown.

## Summary

- **Error Classification**: Iroh uses `RelayConnectionError`, `DialError`, and `RunError` to distinguish between dialing failures, handshake failures, and established connection drops in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs).
- **Timeout Hierarchy**: `CONNECT_TIMEOUT` governs initial connection, `PING_INTERVAL` handles keepalive and send timeouts, and `RELAY_INACTIVE_CLEANUP_TIME` manages resource cleanup.
- **Retry Logic**: The `run()` loop implements exponential backoff (10ms to 16s) for initial failures but resets backoff immediately for dropped established connections.
- **Error Handling**: Applications can downcast `anyhow::Error` to `RelayConnectionError` to implement specific recovery strategies for different failure modes.
- **Configuration**: Timeout constants can be adjusted through the `ClientBuilder` or custom configuration structures to accommodate high-latency or unstable networks.

## Frequently Asked Questions

### What is the default dial timeout in iroh?

The default **dial timeout** is controlled by the `CONNECT_TIMEOUT` constant in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs) (lines 80-85). While the exact value depends on the build configuration, it typically defaults to 10 seconds, covering DNS resolution, TCP/QUIC connection establishment, and TLS handshake completion.

### How does iroh distinguish between initial connection failures and dropped established connections?

Iroh distinguishes these through the `RelayConnectionError` enum variants. A `Dial` or `Handshake` variant indicates the connection never reached an operational state, while an `Established` variant indicates the connection was previously healthy and then failed. The retry logic in `run()` treats these differently: initial failures trigger exponential backoff, while established connection drops reset the backoff and reconnect immediately.

### Can I disable the exponential backoff for relay connections?

The backoff is hardcoded in `build_backoff()` at lines 50-57 of [`actor.rs`](https://github.com/n0-computer/iroh/blob/main/actor.rs) with settings of 10ms minimum and 16s maximum delay. While you cannot disable it without modifying the source code, you can effectively minimize delays by handling errors at the application level and immediately triggering a new `RelayActor` instance when specific error variants occur.

### What happens when a ping timeout occurs in an established relay connection?

When a ping is sent without a corresponding pong within the `PING_INTERVAL` duration, the `run_connected()` or `run_sending()` method returns `RunError::PingTimeout`. This error propagates through the `Established` variant of `RelayConnectionError`, causing the outer `run()` loop to reset the exponential backoff and immediately attempt to re-establish the connection with the relay server.