How to Implement Unreliable Datagram Connections in iroh

Use the Connection::read_datagram() and Connection::send_datagram() methods on an established QUIC connection to exchange small, unordered packets that may be dropped by the congestion controller.

The iroh crate from the n0-computer/iroh repository builds peer-to-peer communication on top of QUIC, exposing low-level primitives for unreliable messaging. While QUIC streams provide ordered, reliable transport, unreliable datagram connections in iroh allow applications to send small, fire-and-forget packets ideal for heartbeats or low-latency control signals. This guide covers the specific API methods and implementation patterns found in the source code.

Understanding QUIC Application Datagrams

QUIC defines application datagrams as small, unordered packets that do not undergo automatic retransmission. In iroh, these map directly to the underlying QUIC implementation (noq_proto), providing a best-effort delivery mechanism.

Key characteristics include:

  • Payloads must fit into a single QUIC packet, typically ≤ 1 KB
  • Data is encoded as a bytes::Bytes buffer
  • The congestion controller may drop datagrams when buffer space is exhausted
  • Ideal for application-level heartbeats, probe packets, or redundant data streams

Core API Methods for Unreliable Datagrams

The Connection type in iroh/src/endpoint/connection.rs exposes five primary methods for datagram handling.

Receiving Datagrams

read_datagram() returns an async future that yields the next received datagram as a bytes::Bytes object. This method blocks until data arrives or the connection closes.

Sending Datagrams

send_datagram() attempts immediate transmission. If the congestion window is full, the method may drop older datagrams to make room. For blocking behavior that waits for buffer space, use send_datagram_wait(), which preserves ordering of older packets by blocking until space becomes available.

Buffer Management

Before sending, query transport limits using max_datagram_size() to determine the largest payload the peer will accept. Check available capacity with datagram_send_buffer_space() to avoid unnecessary drops.

Implementation Workflow

Implementing unreliable datagram connections follows a four-step pattern:

  1. Create an Endpoint with your desired ALPN identifiers and relay configuration
  2. Establish a connection using endpoint.accept() for servers or endpoint.connect() for clients
  3. Read incoming data by awaiting conn.read_datagram() in a dedicated async task
  4. Write outgoing data using conn.send_datagram(payload.into()) or the blocking send_datagram_wait() variant

Because these APIs are fully async, they integrate cleanly with tokio and support full-duplex communication patterns across spawned tasks.

Code Examples

The iroh repository provides complete working examples demonstrating both sides of a datagram exchange.

Server Implementation (listen-unreliable.rs)

The following example from iroh/examples/listen-unreliable.rs accepts incoming connections and echoes responses via unreliable datagrams:

use iroh::{Endpoint, RelayMode, SecretKey, endpoint::presets};
use n0_error::Result;
use tracing::{info, warn};

#[tokio::main]
async fn main() -> Result<()> {
    let endpoint = Endpoint::builder(presets::N0)
        .secret_key(SecretKey::generate())
        .alpns(vec![b"example".to_vec()])
        .relay_mode(RelayMode::Default)
        .bind()
        .await?;

    while let Some(incoming) = endpoint.accept().await {
        let conn = match incoming.accept() {
            Ok(a) => a.await?,
            Err(e) => {
                warn!("incoming failed: {e:#}");
                continue;
            }
        };
        info!("new connection {}", conn.remote_id());

        // Spawn a task that reads a datagram and replies.
        tokio::spawn(async move {
            while let Ok(msg) = conn.read_datagram().await {
                let txt = String::from_utf8(msg.into()).unwrap();
                println!("got: {txt}");
                let reply = format!("ack: {txt}");
                conn.send_datagram(reply.into_bytes().into()).ok();
            }
        });
    }
    Ok(())
}

Client Implementation (connect-unreliable.rs)

This client example from iroh/examples/connect-unreliable.rs establishes a connection and exchanges a single datagram:

use std::{net::SocketAddr, str::FromStr};
use clap::Parser;
use iroh::{Endpoint, EndpointAddr, RelayMode, RelayUrl, SecretKey, endpoint::presets};
use iroh_base::TransportAddr;
use n0_error::Result;

#[derive(Parser)]
struct Cli {
    #[clap(long)] endpoint_id: iroh::EndpointId,
    #[clap(long, value_delimiter = ' ')] addrs: Vec<SocketAddr>,
    #[clap(long)] relay_url: RelayUrl,
}

#[tokio::main]
async fn main() -> Result<()> {
    let args = Cli::parse();
    let endpoint = Endpoint::builder(presets::N0)
        .secret_key(SecretKey::from_bytes(&rand::random()))
        .alpns(vec![b"example".to_vec()])
        .relay_mode(RelayMode::Default)
        .bind()
        .await?;

    // Build the remote address (public key + UDP + relay).
    let remote = EndpointAddr::from_parts(
        args.endpoint_id,
        args.addrs
            .into_iter()
            .map(TransportAddr::Ip)
            .chain(std::iter::once(TransportAddr::Relay(args.relay_url))),
    );

    let conn = endpoint.connect(remote, b"example").await?;
    conn.send_datagram(b"hello from client".to_vec().into())?;
    let reply = conn.read_datagram().await?;
    println!("reply: {}", String::from_utf8(reply.into()).unwrap());
    Ok(())
}

Summary

  • Unreliable datagrams in iroh expose QUIC's application datagram primitive for best-effort messaging
  • Use Connection::read_datagram() to receive incoming packets and Connection::send_datagram() to transmit
  • Check max_datagram_size() and datagram_send_buffer_space() to respect transport limits
  • Reference implementations are available in iroh/examples/listen-unreliable.rs and iroh/examples/connect-unreliable.rs
  • All datagram APIs are located in iroh/src/endpoint/connection.rs and delegate to the internal noq_proto implementation

Frequently Asked Questions

What is the maximum payload size for unreliable datagrams in iroh?

The maximum payload size depends on the peer's QUIC implementation and network path MTU. Use Connection::max_datagram_size() to query the current limit, which typically returns approximately 1 KB or less. Exceeding this size will result in transmission errors.

How do I prevent datagram drops when the congestion window is full?

Use Connection::send_datagram_wait() instead of send_datagram(). The blocking variant waits for buffer space to become available, ensuring older packets are not dropped when the congestion controller limits throughput.

Are unreliable datagrams in iroh ordered with respect to reliable streams?

No. QUIC application datagrams are unordered and may arrive out of sequence relative to each other and to any reliable stream data. They are transmitted on a separate channel that bypasses the stream ordering guarantees.

Where are the datagram methods implemented in the iroh source code?

The primary implementation resides in iroh/src/endpoint/connection.rs around line 916, where methods like send_datagram, send_datagram_wait, read_datagram, and max_datagram_size delegate to the underlying QUIC transport layer (noq_proto).

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 →