# How to Handle Connection Timeouts, Retries, and Failure Scenarios in iroh

> Discover how iroh handles connection timeouts, retries, and failures with its multi-layered strategy. Configure iroh easily and ensure robust network performance.

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

---

**iroh implements a multi-layered timeout and retry strategy across QUIC transport, relay connections, NAT hole-punching, and DNS resolution, exposing fine-grained configuration via builder methods while automatically handling transient network failures internally.**

iroh is a peer-to-peer networking library built on QUIC that must operate reliably over unreliable networks. The n0-computer/iroh codebase embeds specific timeout constants, retry loops, and failure recovery mechanisms at the transport, protocol, and application layers. Understanding these internals allows you to tune connection behavior and handle edge cases effectively.

## Transport-Level Timeouts in the Relay Actor

The relay transport actor protects every outgoing operation with `tokio::time::timeout` guards. In [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs), the implementation defines several critical constants:

- **`CONNECT_TIMEOUT`** (approximately 10 seconds) guards the initial QUIC handshake when establishing a relay connection.
- **`PING_INTERVAL`** drives regular keep-alive traffic, with derived send timeouts preventing stalled operations.
- **`RELAY_INACTIVE_CLEANUP_TIME`** automatically tears down idle connections that exceed the inactivity threshold.

The actor applies these timeouts consistently using the following pattern:

```rust
let result = tokio::time::timeout(CONNECT_TIMEOUT, client_builder.connect()).await;
match result {
    Ok(Ok(conn)) => { /* connection succeeded */ }
    Ok(Err(e))   => { /* connection error – may retry */ }
    Err(_)       => { /* timeout – schedule a retry */ }
}

```

This ensures that hanging operations fail fast and trigger retry logic rather than consuming resources indefinitely.

## Heartbeat Intervals and Retry Loops

At the socket layer, iroh maintains connection liveness through heartbeats. The implementation in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) defines a **`HEARTBEAT_INTERVAL`** of 5 seconds. The codebase comments explain the rationale for a 15-second overall timeout: this duration allows three heartbeats plus several retry attempts before giving up on a peer.

If a heartbeat is missed, the socket implementation retries the operation up to a configurable number of attempts. This retry loop operates transparently—your application code awaits the connection future while the library handles the retransmission logic internally.

## Hole-Punching Retries for NAT Traversal

When direct connections require NAT traversal, iroh employs exponential backoff for hole-punching attempts. The logic resides in [`iroh/src/socket/remote_map/remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map/remote_state.rs).

The remote-state machine schedules new hole-punch attempts using a calculated backoff (`next_hp - now`). Each retry is logged via `trace!` macros, and the system aborts only on fatal errors rather than transient failures. This means that temporary NAT mapping timeouts or firewall issues automatically trigger rediscovery attempts without manual intervention.

To use this functionality, you simply await the connection future; the retry scheduling happens internally:

```rust
let conn = client
    .connect("iroh://peer-with-nat")
    .await?
    .await_connection()  // Blocks until hole-punch succeeds or times out
    .await?;

```

## QUIC Address Validation and RETRY Packets

iroh leverages QUIC's built-in address validation mechanism to prevent amplification attacks and verify client reachability. When the server in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) receives a packet from an unvalidated address, it issues a **RETRY** packet via `Incoming::retry()`.

The client must resend the packet with the provided retry token, as implemented in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs). Only after this token validation succeeds does the server consider the connection established. This built-in retry mechanism operates at the protocol level, independent of application logic, ensuring that spoofed or unreachable addresses cannot consume server resources.

## DNS Lookup Retries

For peer discovery via DNS, the resolver in [`iroh/src/address_lookup/pkarr.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup/pkarr.rs) implements per-attempt timeouts with progressive backoff. When a lookup fails, the system schedules a new attempt after a delay calculated as:

```rust
retry_after = Duration::from_secs(failed_attempts)

```

This means the first retry waits 1 second, the second waits 2 seconds, and so on, preventing thundering herds while ensuring eventual consistency for address resolution.

## Configuring Timeouts in Your Application

While iroh handles retries automatically, you can customize timeout values through the public API. The builder methods in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs) expose configuration options like `set_connect_timeout` and `set_ping_interval`.

Here is a complete example showing how to configure timeouts and handle failure scenarios:

```rust
use iroh::client::ClientBuilder;
use std::time::Duration;
use tokio::time::timeout;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Build a client with custom timeout values
    let client = ClientBuilder::default()
        .connect_timeout(Duration::from_secs(8))   // QUIC handshake timeout
        .ping_interval(Duration::from_secs(4))     // Keep-alive interval
        .build()
        .await?;

    // Attempt a connection with an explicit timeout
    let connect_fut = client.connect("iroh://example.org");
    match timeout(Duration::from_secs(10), connect_fut).await {
        Ok(Ok(conn)) => {
            println!("Connected!");
            // Use the connection …
        }
        Ok(Err(e)) => {
            eprintln!("Connection failed: {e}");
            // Optional: retry with back-off
        }
        Err(_) => {
            eprintln!("Connection timed out");
            // Optional: schedule a retry after a delay
        }
    }

    // Hole-punching retries are handled internally by RemoteState
    let conn = client
        .connect("iroh://peer-with-nat")
        .await?
        .await_connection()
        .await?;
    println!("Direct path established!");
    Ok(())
}

```

Key implementation details to note:
- `connect_timeout` and `ping_interval` map directly to the internal constants used in [`actor.rs`](https://github.com/n0-computer/iroh/blob/main/actor.rs).
- The outer `tokio::time::timeout` wrapper mirrors the library's own timeout guards, allowing you to apply additional wall-clock limits.
- The `await_connection()` method blocks until hole-punching succeeds or the internal timeout triggers.

## Summary

- **Transport timeouts**: The relay actor uses `CONNECT_TIMEOUT` (10s) and `PING_INTERVAL` with `tokio::time::timeout` to prevent hanging operations.
- **Heartbeat retries**: A 5-second heartbeat interval with a 15-second overall deadline allows multiple retry attempts before declaring a peer unreachable.
- **NAT traversal**: The `RemoteState` machine in [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs) schedules hole-punch retries with calculated backoff delays.
- **Protocol validation**: QUIC RETRY packets validate client addresses before committing resources, implemented in [`protocol.rs`](https://github.com/n0-computer/iroh/blob/main/protocol.rs) and [`connection.rs`](https://github.com/n0-computer/iroh/blob/main/connection.rs).
- **DNS resilience**: The pkarr resolver uses progressive backoff (`retry_after = Duration::from_secs(failed_attempts)`) for lookup retries.
- **Configuration**: Use `ClientBuilder` methods like `connect_timeout` and `ping_interval` to tune behavior, or wrap futures in external timeouts for additional control.

## Frequently Asked Questions

### How long does iroh wait before timing out a connection?

iroh applies different timeouts depending on the operation. The initial QUIC handshake uses a `CONNECT_TIMEOUT` of approximately 10 seconds, while the overall connection establishment (including hole-punching) allows roughly 15 seconds to accommodate three heartbeat intervals and multiple retry attempts. Idle relay connections are cleaned up after `RELAY_INACTIVE_CLEANUP_TIME`.

### Does iroh automatically retry failed connections?

Yes, iroh handles retries automatically at multiple layers. The socket layer retries missed heartbeats, the remote-state machine retries hole-punching attempts with backoff, and the DNS resolver retries failed lookups with progressively increasing delays. You typically await the connection future and handle only the final `Result`, though you can layer additional retry logic using `tokio::time::timeout` if needed.

### How do I customize timeout values in iroh?

Use the builder methods exposed in the public API, such as `ClientBuilder::connect_timeout()` and `ClientBuilder::ping_interval()`, to adjust the internal constants. For additional control, wrap connection futures in `tokio::time::timeout` to enforce stricter wall-clock deadlines than the library defaults.

### What happens when hole-punching fails?

If NAT traversal fails after exhausting the retry budget, the connection attempt returns an error. However, iroh will have attempted multiple hole-punches with calculated backoff delays (as implemented in [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs)) before giving up. Your application can catch this error and decide whether to retry the entire connection flow, fall back to relay-only mode, or fail permanently.