# How to Use 0-RTT Connections in iroh: A Complete Guide

> Learn how to use 0-RTT connections in iroh with our complete guide. Enable faster data transmission before the QUIC handshake completes using the into_0rtt method.

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

---

**iroh supports 0-RTT (Zero Round-Trip Time) connections through the `into_0rtt` method on both client-side `Connecting` futures and server-side `Accepting` objects, allowing data transmission before the QUIC handshake completes.**

The n0-computer/iroh repository provides QUIC-based networking with early data support. This guide explains how to implement 0-RTT connections in iroh using the actual source APIs found in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) and demonstrated in [`iroh/examples/0rtt.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/0rtt.rs).

## Understanding 0-RTT in QUIC

0-RTT enables clients to send application data immediately without waiting for the full TLS handshake to complete. This reduces latency for connection resumption scenarios where the client possesses valid TLS tickets from a previous session.

In iroh, the implementation splits the connection lifecycle into two phases: the initial 0-RTT phase where data flows over an unverified connection, and the post-handshake phase where the connection either becomes fully established or falls back to a standard 1-RTT connection.

## Client-Side Implementation

### Initiating a 0-RTT Connection

To begin a 0-RTT connection, the client first creates a `Connecting` future using `endpoint.connect_with_opts()`, then immediately attempts conversion using `into_0rtt()`.

```rust
let connecting = endpoint
    .connect_with_opts(remote_id, PINGPONG_ALPN, Default::default())
    .await?;

match connecting.into_0rtt() {
    Ok(zrtt) => {
        // Send data immediately via 0-RTT
        let (send, recv) = zrtt.open_bi().await?;
        let zrtt_task = tokio::spawn(ping(send, seq));
        // Handle handshake result...
    }
    Err(connecting) => {
        // No 0-RTT possible, fall back to normal connection
        let conn = connecting.await?;
    }
}

```

If `into_0rtt()` returns `Ok(zrtt)`, you receive an `OutgoingZeroRttConnection` that allows immediate stream creation via `open_bi()` or `open_uni()`.

### Handling Handshake Completion

The `OutgoingZeroRttConnection` provides `handshake_completed()`, which returns a `ZeroRttStatus` indicating whether the server accepted or rejected the 0-RTT attempt.

```rust
match zrtt.handshake_completed().await? {
    ZeroRttStatus::Accepted(conn) => {
        // 0-RTT accepted, connection upgraded to full Connection
        zrtt_task.await?;
        pong(recv, seq).await?;
        conn
    }
    ZeroRttStatus::Rejected(conn) => {
        // 0-RTT rejected, abort pending operations and use fallback
        zrtt_task.abort();
        let (send, recv) = conn.open_bi().await?;
        pingpong(send, recv, seq).await?;
        conn
    }
}

```

When rejected, any data sent during the 0-RTT phase may be discarded by the server, requiring application-level retry logic on the standard connection.

## Server-Side Implementation

### Accepting 0-RTT Connections

The server side processes incoming connections through `endpoint.accept().await`, then calls `into_0rtt()` on the `Accepting` object to enable early data acceptance.

```rust
while let Some(incoming) = endpoint.accept().await {
    tokio::spawn(async move {
        let accepting = incoming.accept()?;
        let connection = accepting.into_0rtt(); // Enable 0-RTT if possible
        
        let (mut send, mut recv) = connection.accept_bi().await?;
        // Process stream...
    });
}

```

The server must advertise the appropriate ALPN (such as `"0rtt-pingpong"`) via the endpoint builder to indicate 0-RTT support for specific protocols.

### Detecting 0-RTT Streams

Individual streams expose whether they arrived via 0-RTT through the `is_0rtt()` method on `RecvStream`, allowing servers to apply different validation logic for early data.

```rust
trace!("recv.is_0rtt: {}", recv.is_0rtt());

```

This flag returns `true` for streams that were opened during the unverified 0-RTT phase, permitting security-sensitive applications to defer processing of certain requests until the handshake completes.

## Configuration and Tuning

TLS ticket caching determines how many concurrent clients can resume connections via 0-RTT. Configure the cache size using `max_tls_tickets` on the endpoint builder (default is 256).

```rust
let endpoint = iroh::Endpoint::builder(presets::N0)
    .alpns(vec![PINGPONG_ALPN.to_vec()])
    .max_tls_tickets(512) // Increase for high-concurrency scenarios
    .bind()
    .await?;

```

According to the implementation in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs), increasing this value supports more concurrent 0-RTT clients but consumes additional memory for ticket storage.

## Complete Example

The repository provides a full working implementation in [`iroh/examples/0rtt.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/0rtt.rs) demonstrating both client and server patterns with proper error handling and fallback logic. This example implements a ping-pong protocol that measures the latency benefits of 0-RTT resumption versus standard connections.

## Summary

- **0-RTT connections** in iroh allow immediate data transmission before QUIC handshake completion, reducing latency for resumed sessions.
- Use `Connecting::into_0rtt()` on the client and `Accepting::into_0rtt()` on the server to enable early data flows.
- Handle `ZeroRttStatus::Accepted` and `ZeroRttStatus::Rejected` via `handshake_completed()` to manage connection lifecycle transitions.
- Check `RecvStream::is_0rtt()` to identify streams that arrived during the unverified phase.
- Configure `max_tls_tickets` in the endpoint builder to tune TLS ticket cache size for your deployment scale.
- Reference [`iroh/examples/0rtt.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/0rtt.rs) and [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) for production-ready implementations.

## Frequently Asked Questions

### What happens if the server rejects a 0-RTT connection?

When the server rejects 0-RTT, `handshake_completed()` returns `ZeroRttStatus::Rejected` with a standard `Connection` object. Any data sent during the 0-RTT phase may be discarded, so applications should abort pending 0-RTT tasks and retry on the fallback connection using standard streams.

### How do I know if a specific stream was sent via 0-RTT?

Incoming streams expose the `is_0rtt()` method on the `RecvStream` type. According to the implementation in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs), this boolean flag returns `true` if the stream was opened during the 0-RTT phase before handshake completion, allowing servers to apply appropriate validation logic.

### Can I use 0-RTT with any ALPN protocol?

No, both client and server must agree on the ALPN. The server must advertise 0-RTT-capable protocols via the endpoint builder's `alpns` method, and the client must specify the same ALPN when calling `connect_with_opts()`. The ticket cache must also contain valid entries from previous connections to the same endpoint.

### What is the default TLS ticket cache size and when should I change it?

The default `max_tls_tickets` value is 256, as defined in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs). Increase this value for deployments handling thousands of concurrent clients resuming connections frequently, or decrease it to reduce memory consumption when 0-RTT resumption is rare.