# How to Handle Iroh Connection Timeouts and Retry Logic

> Master Iroh connection timeouts and retry logic. Learn how Iroh automatically retries critical operations and configures timeouts using constants and a public builder API for reliable P2P networking.

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

---

**Iroh implements a multi-layered timeout and retry strategy using transport-level constants like `CONNECT_TIMEOUT` (10s) and `HEARTBEAT_INTERVAL` (5s), with automatic retries for hole-punching, QUIC address validation, and DNS lookups, all configurable via the public builder API.**

Iroh is a peer-to-peer networking library from n0-computer that establishes connections over QUIC. Because real-world networks are unpredictable, the library embeds sophisticated timeout and retry mechanisms at multiple layers—from transport-level handshakes to NAT traversal. Understanding these internals helps you tune performance and handle transient failures gracefully.

## Transport-Level Timeouts

The relay actor protects every outgoing operation with `tokio::time::timeout`. In [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs), three core constants govern connection health:

- **`CONNECT_TIMEOUT`** (≈10 seconds): Guards the initial QUIC handshake
- **`PING_INTERVAL`**: Drives regular keep-alive traffic with a derived send timeout
- **`RELAY_INACTIVE_CLEANUP_TIME`**: Automatically tears down idle connections

When any operation exceeds its limit, the actor triggers cleanup logic and logs the failure before attempting a retry.

## Heartbeat and Retry Loops

The socket implementation uses a `HEARTBEAT_INTERVAL` of 5 seconds to maintain live connections. As noted in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs), the code enforces an overall 15-second timeout to allow three heartbeats and several retry chances.

This design means:
1. The client sends a heartbeat every 5 seconds
2. If a heartbeat is missed, the retry counter increments
3. After exhausting configurable retry attempts, the connection is marked failed

## Hole-Punching Retries

When NAT traversal is required, the remote-state machine schedules new hole-punch attempts using calculated back-off. The logic 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) implements the following flow:

- Calculates `next_hp - now` to determine the next attempt time
- Logs each retry with `trace!` for observability
- Aborts only on fatal errors, continuing to retry on transient failures

You typically interact with this indirectly by awaiting the connection future, which blocks until hole-punching succeeds or the global timeout expires.

## QUIC Address Validation Retries

Iroh uses QUIC's built-in RETRY packet to validate client source addresses. According to [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) and [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs), the flow works as follows:

1. The server receives a packet from an unvalidated address
2. It issues a retry token via `Incoming::retry()`
3. The client must resend the packet with the token
4. Once validated, the connection proceeds normally

This mechanism prevents connection flooding while allowing legitimate clients to retry automatically.

## DNS Lookup Retries

The DNS resolver in [`iroh/src/address_lookup/pkarr.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup/pkarr.rs) applies per-attempt timeouts with exponential back-off. The retry logic calculates:

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

```

Each failed attempt increments the counter, increasing the delay before the next lookup. This prevents overwhelming DNS servers while ensuring eventual resolution.

## Configuring Timeouts via the Builder API

Iroh exposes timeout configuration through the endpoint builder in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs). You can tune behavior to match your network environment:

```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 is handled internally; await the connection future
    let conn = client
        .connect("iroh://peer-with-nat")
        .await?
        .await_connection()                     // blocks until hole-punch succeeds or times out
        .await?;
    println!("Direct path established!");
    Ok(())
}

```

Key implementation details:
- `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` mirrors the library's own guards, letting you apply additional wall-clock limits
- Iroh automatically performs NAT-traversal retries; you only need to handle the final `Result`

## Summary

- **Transport timeouts**: `CONNECT_TIMEOUT` (10s) and `RELAY_INACTIVE_CLEANUP_TIME` guard the relay actor in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs)
- **Heartbeat logic**: `HEARTBEAT_INTERVAL` (5s) with a 15s overall timeout defined in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs)
- **NAT traversal**: Automatic retries with back-off calculated 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)
- **QUIC validation**: Address validation via `Incoming::retry()` in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) and [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs)
- **DNS resolution**: Exponential back-off in [`iroh/src/address_lookup/pkarr.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup/pkarr.rs)
- **Configuration**: Builder methods like `set_connect_timeout` and `set_ping_interval` in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs)

## Frequently Asked Questions

### How long does Iroh wait before timing out a connection attempt?

Iroh uses a default `CONNECT_TIMEOUT` of approximately 10 seconds for the initial QUIC handshake, as defined in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs). Additionally, the socket layer enforces a 15-second overall timeout to accommodate three heartbeat intervals (5 seconds each) plus retry attempts.

### Can I customize the timeout values in Iroh?

Yes. The public API exposes builder methods such as `connect_timeout()` and `ping_interval()` through `ClientBuilder` and endpoint configuration in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs). You can also wrap any async operation with `tokio::time::timeout` to apply application-level limits.

### What happens when hole-punching fails?

When NAT traversal fails initially, the `RemoteState` machine 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) schedules a retry after a calculated back-off period (`next_hp - now`). The system logs each attempt via `trace!` and continues retrying until the connection succeeds or encounters a fatal error.

### How does Iroh handle DNS lookup failures?

The DNS resolver in [`iroh/src/address_lookup/pkarr.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup/pkarr.rs) implements a retry mechanism with exponential back-off. After each failed attempt, it waits `Duration::from_secs(failed_attempts)` before retrying, preventing server overload while ensuring eventual resolution.