# How to Handle Connection timeouts and retries in iroh: A Complete Guide

> Master connection timeouts and retries in iroh. Learn how its multi-layered strategy handles QUIC, heartbeats, hole-punching, and DNS with configurable intervals for robust performance.

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

---

**iroh implements a multi-layered timeout and retry strategy across QUIC transport, heartbeat loops, hole-punching state machines, and DNS resolution, with all intervals configurable via the endpoint builder API.**

Handling connection timeouts and retries in iroh requires understanding its peer-to-peer QUIC stack. The `n0-computer/iroh` repository embeds specific timeout constants and retry logic at multiple architectural layers to ensure robust connectivity across unreliable networks.

## Transport-Level Timeouts and Keep-Alives

iroh uses `tokio::time::timeout` wrappers to guard every critical operation in the relay actor. These protections cover the initial handshake, periodic health checks, and idle connection cleanup.

### QUIC Handshake and Connection Timeouts

The `CONNECT_TIMEOUT` constant (approximately 10 seconds) in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs) defines how long the relay transport waits for a QUIC handshake to complete.

```rust
// Pattern found in iroh/src/socket/transports/relay/actor.rs
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 */ }
}

```

If the handshake exceeds this duration, the operation returns a timeout error, triggering the caller's retry logic.

### Heartbeat Intervals and Inactive Cleanup

The socket implementation defines a `HEARTBEAT_INTERVAL` of 5 seconds in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs). The code uses a 15-second overall timeout window to allow for three heartbeat cycles plus retry attempts before marking a connection as failed.

An additional `RELAY_INACTIVE_CLEANUP_TIME` automatically tears down idle relay connections to prevent resource exhaustion.

## Hole-Punching and NAT Traversal Retries

When direct connectivity requires NAT hole-punching, iroh schedules retries with calculated back-offs. 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 computes a `next_hp` (next hole-punch) timestamp. If the current time exceeds this value, it triggers a new attempt and logs the retry via `trace!` macros. The system aborts only on fatal errors, continuing to retry transient failures until success or explicit cancellation.

## QUIC Address Validation and Retry Packets

iroh leverages QUIC's built-in **RETRY** packet mechanism to validate client source addresses and prevent amplification attacks. This flow spans [`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).

When a server receives an initial packet from an unvalidated address, it calls `Incoming::retry()` to issue a token. The client must resend the packet with this token via the `retry()` method on the connection. After successful validation, the connection proceeds normally.

## DNS Lookup Resilience

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 per-attempt timeouts with progressive back-offs. On resolution failure, it calculates:

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

```

This linear back-off ensures that repeated DNS failures do not overwhelm the resolver while maintaining aggressive retry intervals for transient network glitches.

## Configuring Timeouts in Your Application

While iroh's internal mechanisms handle most retry logic automatically, you can customize timeout values through the builder API in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs).

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

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

    // Apply an explicit wall-clock timeout to any connect operation
    let connect_fut = client.connect("iroh://example.org");
    match timeout(Duration::from_secs(10), connect_fut).await {
        Ok(Ok(conn)) => {
            println!("Connected successfully");
        }
        Ok(Err(e)) => {
            eprintln!("Connection failed: {e}");
            // Implement application-specific retry logic here
        }
        Err(_) => {
            eprintln!("Connection timed out after 10s");
            // Handle timeout with custom back-off strategy
        }
    }

    // For NAT-traversed peers, await_connection handles internal retries
    let conn = client
        .connect("iroh://peer-with-nat")
        .await?
        .await_connection()
        .await?;
    
    Ok(())
}

```

The `connect_timeout` and `ping_interval` methods map directly to the internal constants used in the relay actor. The `await_connection()` method on the connection future automatically manages hole-punching retries; your application awaits only the final result.

## Summary

- **Multi-layered protection**: iroh applies timeouts at the QUIC handshake, heartbeat, hole-punching, and DNS resolution layers.
- **Configurable constants**: Use `ClientBuilder` methods like `connect_timeout()` and `ping_interval()` to tune defaults defined in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs).
- **Automatic hole-punching**: 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 retries with back-offs without requiring manual intervention.
- **QUIC security**: Address validation uses `Incoming::retry()` in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs) to prevent spoofed connection attempts.
- **DNS resilience**: The pkarr resolver implements linear back-off (`Duration::from_secs(failed_attempts)`) for transient lookup failures.

## Frequently Asked Questions

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

The default `CONNECT_TIMEOUT` is approximately 10 seconds, defined in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs). This value guards the initial QUIC handshake phase. You can override it via `ClientBuilder::connect_timeout()`.

### How does iroh handle NAT traversal failures?

iroh automatically retries hole-punching attempts using a back-off strategy 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). The system computes a `next_hp` timestamp and retries after the delay expires, logging attempts via `trace!` macros until either success or a fatal error occurs.

### Can I customize retry intervals without modifying the source code?

Yes. The public API exposes timeout configuration through builder methods such as `set_connect_timeout`, `set_ping_interval`, and others in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs). For application-level control, wrap connection futures in `tokio::time::timeout` to apply custom wall-clock limits.

### What happens when a QUIC retry token validation fails?

If address validation fails during the QUIC handshake, the server sends a RETRY packet containing a token via `Incoming::retry()` in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs). The client must resend the packet using the `retry()` method in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs). Failure to provide a valid token results in connection termination.