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

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 and demonstrated in 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().

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.

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.

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.

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).

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, 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 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 and 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, 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →