# When Does iroh Hole-Punching Fall Back to Relay Servers?

> Discover when iroh hole-punching relies on relay servers. Learn about NAT traversal failure conditions, symmetric NAT detection, and timeout scenarios for robust connectivity.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: internals
- Published: 2026-07-14

---

**iroh falls back to relay servers only after the remote-state actor conclusively determines that direct NAT traversal is impossible, specifically when all hole-punching candidates fail, symmetric NAT is detected, retry intervals exhaust without new address candidates, or direct paths exceed idle timeouts.**

n0-computer/iroh is a Rust implementation of the Interplanetary Relay Overlay for Hole-punching that prioritizes direct peer-to-peer connections before resorting to relay infrastructure. Understanding precisely when iroh's hole-punching falls back to relay servers is essential for diagnosing connectivity failures and optimizing network performance in complex NAT environments.

## Failure Conditions That Trigger Relay Fallback

The decision to abandon direct connection attempts occurs within [`iroh/src/socket/remote_map/remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map/remote_state.rs), where the **remote-state actor** continuously evaluates path viability.

### Exhausted Direct Path Candidates

The `RemotePathState` logic tracks path viability through the `FailedHolePunch` status. When the `prune_failed_holepunch` handling determines that all direct paths have failed hole-punching attempts, the actor discards these unusable paths. The system then scans for remaining paths containing only relay addresses, triggering an immediate fallback to relay servers.

### Symmetric NAT Detection

When the underlying NAT-traversal module returns a `HolePunchResult::SymmetricNat`, this result bubbles up to the remote-state actor. **Symmetric NATs** cannot be traversed via standard hole-punching techniques, so the actor immediately marks affected paths as relay-only, bypassing further direct connection attempts in favor of the configured relay infrastructure.

### Retry Interval Timeouts

The constant `HOLEPUNCH_ATTEMPTS_INTERVAL` (set to 5 seconds) governs retry scheduling in [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs). If no new NAT address candidates appear between intervals, the actor skips the next hole-punch attempt and falls back to the relay path rather than attempting connections deemed likely to fail.

### Path Idle Timeouts and Pruning

Direct paths that remain idle longer than `RELAY_PATH_MAX_IDLE_TIMEOUT` (default 30 seconds) are pruned by the remote-state actor. Once pruned or marked stale, the actor replaces the direct path with a relay path to maintain connectivity through the relay server.

## Transport Layer Relay Establishment

When the remote-state actor selects a relay-only address, the transport layer creates a **RelayPath** as implemented in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs). The endpoint must be configured with `RelayMode::Custom(relay_map)` through the **EndpointBuilder**, typically initialized via test utilities like `run_relay_server` found in [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs).

## Implementation Code Examples

Configure an endpoint to support automatic relay fallback:

```rust
let relay_map = /* obtained from run_relay_server() */;
let builder = iroh::endpoint::Builder::default()
    .relay_mode(iroh::relay::RelayMode::Custom(relay_map));
let endpoint = builder.bind().await?;

```

Remote-state actor logic that selects relay paths after failed attempts:

```rust
// Inside RemoteStateActor
if self.state.paths.iter().all(|p| p.is_failed_holepunch()) {
    // No viable direct path → pick a relay‑only address
    self.select_best_path(|p| p.is_relay());
}

```

Testing symmetric NAT fallback behavior:

```rust
let (lab, relay_map, _guard, _lab_guard) = lab_with_relay(testdir!()).await?;
let pair = Pair::new(relay_map);
let conn = pair.connect().await?;
assert!(is_relayed(&conn), "connection should be relayed");

```

## Summary

- iroh attempts direct hole-punching before considering relay servers, as implemented in the n0-computer/iroh codebase.
- Fallback triggers when `FailedHolePunch` status affects all direct paths 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).
- Symmetric NAT detection immediately forces relay-only mode without retry attempts.
- The 5-second `HOLEPUNCH_ATTEMPTS_INTERVAL` and 30-second `RELAY_PATH_MAX_IDLE_TIMEOUT` constants control fallback timing.
- The transport layer creates `RelayPath` instances only after the remote-state actor confirms direct traversal is impossible via `add_failed_holepunch` handling and path pruning logic.

## Frequently Asked Questions

### How does iroh detect that hole-punching has failed?

The remote-state actor monitors path status through `RemotePathState` 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). When paths accumulate `FailedHolePunch` status through `add_failed_holepunch` handling and are pruned via the `prune_failed_holepunch` logic, the system recognizes that direct traversal is impossible and initiates relay fallback.

### Can I configure the timeout before iroh falls back to relays?

Yes, the `RELAY_PATH_MAX_IDLE_TIMEOUT` constant defaults to 30 seconds, controlling when idle direct paths are pruned and replaced with relay connections. Additionally, the `HOLEPUNCH_ATTEMPTS_INTERVAL` of 5 seconds governs how long the system waits for new NAT candidates before skipping direct attempts.

### Does symmetric NAT always force relay usage?

Yes, according to the source code, when `HolePunchResult::SymmetricNat` is returned by the NAT-traversal module, the remote-state actor immediately marks the path as relay-only because symmetric NATs prevent successful hole-punching, making relay servers the only viable connection method.

### Where is the relay server configured in iroh?

Relay configuration occurs through `iroh::relay::RelayMode::Custom(relay_map)` passed to the `iroh::endpoint::Builder`, typically sourced from production relay maps or the test utility `run_relay_server` in [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs) (lines 493-521).