# Iroh Connection Lifecycle: From Endpoint Creation to QUIC Handshake Completion

> Explore the iroh connection lifecycle from endpoint creation to QUIC handshake completion. Understand the Rust types guiding QUIC handshake stages.

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

---

**The iroh connection lifecycle progresses through distinct Rust types—`Connecting`, `Accepting`, and `Connection<HandshakeCompleted>`—that map directly to QUIC handshake stages, starting from `Endpoint` creation and ending with graceful closure via `close()` or handle drops.**

The `iroh` crate builds networking on top of QUIC via the `noq` library, exposing a type-safe API that mirrors the underlying protocol phases. Understanding the **iroh connection lifecycle** is essential for building robust peer-to-peer applications that handle both outgoing dials and incoming accepts efficiently. This guide examines the actual implementation in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs), covering each stage from initial endpoint binding to final connection teardown.

## The Iroh Connection Lifecycle Stages

### Stage 1: Endpoint Creation and Binding

Every connection begins with an **Endpoint**. Calling `Endpoint::builder()` followed by `bind()` creates a local QUIC endpoint capable of both dialing and listening. This initialization allocates the underlying UDP socket and configures the TLS stack, preparing the endpoint to handle the connection lifecycle.

### Stage 2: Outgoing Connections with `Connecting`

When dialing a remote peer, `Endpoint::connect()` returns a **`Connecting`** future. Defined at line 138 of [`connection.rs`](https://github.com/n0-computer/iroh/blob/main/connection.rs), this struct represents an outgoing connection that has not yet completed the TLS handshake. Polling this future blocks until the cryptographic handshake finishes, at which point it transitions to the next state.

### Stage 3: Optional 0-RTT for Outgoing Connections

Before the handshake completes, you can convert `Connecting` into a zero-round-trip connection. The `into_0rtt()` method (lines 528-549) returns `Result<OutgoingZeroRttConnection, Connecting>`. On success, you receive an **`OutgoingZeroRttConnection`** (a `Connection<OutgoingZeroRtt>`) that allows immediate data transmission before handshake confirmation, reducing latency for the first request.

### Stage 4: Handshake Completion to `Connection<HandshakeCompleted>`

Once the TLS handshake succeeds, `Connecting` resolves to a **`Connection<HandshakeCompleted>`**. This generic struct, defined at line 735 of [`connection.rs`](https://github.com/n0-computer/iroh/blob/main/connection.rs), represents a fully established, secure connection ready for data transfer. The default generic state parameter is `HandshakeCompleted`, which provides access to the complete connection API.

### Stage 5: Incoming Connections via `Incoming` and `Accepting`

On the server side, `Endpoint::accept()` yields an **`Incoming`** future (lines 129-132). Calling `Incoming::accept()` produces an **`Accepting`** state (lines 140-144), the server-side counterpart to `Connecting`. Polling `Accepting` eventually yields a `Connection<HandshakeCompleted>` after the cryptographic handshake finishes, mirroring the outgoing connection flow.

### Stage 6: Optional 0-RTT on the Server Side

Similar to outgoing connections, incoming connections support early data. After obtaining `Accepting`, calling `into_0rtt()` (lines 594-617) returns an **`IncomingZeroRttConnection`** (`Connection<IncomingZeroRtt>`). This allows the server to process requests before handshake completion, with the connection later resolving to a standard `Connection<HandshakeCompleted>` once the handshake succeeds.

### Stage 7: Data Transfer and Stream Management

Established connections expose methods for data exchange. The `open_bi()` method (line 232) creates bidirectional streams, while `open_uni()` creates unidirectional streams. These methods are available on `Connection<HandshakeCompleted>` and the zero-RTT variants, allowing you to send datagrams or open streams once the connection is established.

### Stage 8: Path Management and Network Discovery

QUIC connections in iroh manage multiple network paths simultaneously. The `paths()` method (line 995) returns a `PathList` showing discovered routes, including relay and direct connections. Use `paths_stream()` or `path_events()` to observe path changes as the connection migrates between networks, which is crucial for maintaining connectivity across network changes.

### Stage 9: Graceful Connection Closure

To shut down a connection, call `close()` (line 912), passing a `VarInt` error code and a reason byte slice. Alternatively, dropping all strong handles closes the connection implicitly. For monitoring closure without keeping the connection alive, use `WeakConnectionHandle`, whose `upgrade()` method (line 1248) returns `Option<Connection>`—yielding `None` after the connection closes.

## Complete Code Example

The following example demonstrates the full lifecycle, including endpoint creation, dialing with optional 0-RTT, accepting incoming connections, and graceful closure:

```rust
use iroh::{Endpoint, presets};

// 1️⃣ Create an endpoint (listen on a random port)
let ep = Endpoint::builder(presets::Minimal)
    .bind()
    .await?;

// -------------------------------------------------
// 2️⃣ Outgoing connection (dial)
let conn_fut = ep.connect(remote_addr, b"my/alpn");

// 3️⃣ Optional 0-RTT before the handshake finishes
let conn_fut = match conn_fut.await?.into_0rtt() {
    Ok(out0rtt) => out0rtt,           // we have a zero-RTT connection
    Err(connecting) => connecting,    // fall back to normal connecting
};

// 4️⃣ Await the handshake (now we have a fully-established connection)
let conn = conn_fut.await?;   // `Connection<HandshakeCompleted>`

// -------------------------------------------------
// 5️⃣ Incoming side – accept a new connection
if let Some(incoming) = ep.accept().await {
    let accepting = incoming.accept()?;          // <- `Accepting`
    let conn = accepting.await?;                // <- `Connection<HandshakeCompleted>`
}

// -------------------------------------------------
// 6️⃣ Use the connection (open a bidirectional stream)
let (mut send, mut recv) = conn.open_bi().await?;
send.write_all(b"hello").await?;
send.finish().await?;
let payload = recv.read_to_end(1024).await?;

// -------------------------------------------------
// 7️⃣ Close the connection gracefully
conn.close(noq::VarInt::from_u32(0), b"done");

```

## Summary

- The lifecycle starts with `Endpoint::builder().bind()` and proceeds through type-specific states that enforce correct usage at compile time.
- Outgoing connections progress through `Connecting` → optional `OutgoingZeroRttConnection` → `Connection<HandshakeCompleted>`.
- Incoming connections progress through `Incoming` → `Accepting` → optional `IncomingZeroRttConnection` → `Connection<HandshakeCompleted>`.
- Data transfer uses `open_bi()`, `open_uni()`, and datagram methods on established connections, with path management via `paths()` and related methods.
- Connections close via `close(error_code, reason)` or handle drops, with `WeakConnectionHandle` available for monitoring without keeping the connection alive.

## Frequently Asked Questions

### What is the difference between `Connecting` and `Accepting` in iroh?

`Connecting` (line 138 in [`connection.rs`](https://github.com/n0-computer/iroh/blob/main/connection.rs)) represents the client-side future awaiting an outgoing handshake, while `Accepting` (lines 140-144) represents the server-side state after `Incoming::accept()` is called. Both eventually resolve to `Connection<HandshakeCompleted>` but originate from different sides of the connection establishment—client-initiated versus server-accepted.

### How does 0-RTT work in the iroh connection lifecycle?

0-RTT allows data transmission before the TLS handshake completes. For outgoing connections, call `Connecting::into_0rtt()` (lines 528-549) to get an `OutgoingZeroRttConnection`. For incoming connections, call `Accepting::into_0rtt()` (lines 594-617) to get an `IncomingZeroRttConnection`. These zero-RTT variants later transition to standard `Connection<HandshakeCompleted>` once the handshake finishes, enabling low-latency communication.

### When should I use `WeakConnectionHandle` instead of `Connection`?

Use `WeakConnectionHandle` when you need to monitor a connection's lifecycle without preventing it from closing. Unlike `Connection`, which keeps the connection alive when held, `WeakConnectionHandle` allows the connection to close when all strong handles drop. Call `upgrade()` (line 1248) to attempt obtaining a `Connection`; it returns `None` if the connection has already closed.

### Where does the actual QUIC state machine implementation live in iroh?

The state machine lives in the underlying `noq` crate, which provides the core QUIC protocol implementation. The `iroh` crate in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) provides safe Rust abstractions over `noq`, exposing the connection lifecycle through types like `Connecting`, `Accepting`, and `Connection<State>` that map cleanly to the underlying QUIC handshake phases.