How to Use iroh’s Datagram Transport for Unreliable Message Delivery

Iroh exposes QUIC’s unreliable datagram frames through Connection::send_datagram and Connection::read_datagram, providing fire-and-forget messaging that works over both direct UDP and relay connections without application-level changes.

The n0-computer/iroh repository implements a QUIC-based networking stack that supports both reliable streams and unreliable datagram transport. This guide explains how to use iroh’s datagram API for low-latency messaging where occasional packet loss is acceptable, such as sensor updates, keep-alives, or real-time gaming packets.

Understanding QUIC Datagrams in iroh

Iroh’s datagram API provides thin wrappers around QUIC’s unreliable, unordered datagram frames. According to the source code in iroh/src/endpoint/connection.rs, a datagram is a single-packet payload that:

  • May be lost – the network can drop it without retransmission.
  • May arrive out of order – there is no sequencing guarantee.
  • Must fit in one QUIC packet – typically limited to a few kilobytes (check max_datagram_size()).

Because of these properties, datagrams are ideal for latency-sensitive traffic where guaranteed delivery is less important than speed. The transport layer handles MTU discovery, but packet fragmentation is not performed.

The Connection Datagram API

The Connection struct in iroh/src/endpoint/connection.rs exposes three primary methods for datagram handling:

  • send_datagram(data) – Fire-and-forget send that returns immediately (or errors if the buffer is full).
  • send_datagram_wait(data) – Waits for buffer space under congestion control before sending.
  • read_datagram() – Returns an async stream that yields inbound bytes::Bytes payloads.
// From iroh/src/endpoint/connection.rs
pub fn read_datagram(&self) -> ReadDatagram<'_> { 
    self.inner.read_datagram() 
}

pub fn send_datagram(&self, data: bytes::Bytes) -> Result<(), SendDatagramError> {
    self.inner.send_datagram(data)
}

When NATs or firewalls prevent direct UDP communication, iroh automatically falls back to a relay server. As implemented in iroh/src/socket/transports/relay/actor.rs, the datagram API works identically over the relay, so application code does not need to distinguish between direct and relayed paths.

Building a Datagram Server

To receive unreliable messages, create an Endpoint, accept incoming connections, and poll read_datagram(). This example from iroh/examples/listen-unreliable.rs demonstrates an echo server:

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

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let endpoint = Endpoint::builder(presets::N0)
        .secret_key(SecretKey::generate())
        .relay_mode(RelayMode::Default)
        .bind()
        .await?;

    // Accept incoming QUIC connections
    while let Some(incoming) = endpoint.accept().await {
        let conn = match incoming.accept() {
            Ok(c) => c,
            Err(e) => {
                warn!("incoming connection failed: {e}");
                continue;
            }
        };
        info!("new unreliable connection from {}", conn.remote_id());

        // Spawn a task that echoes back any datagram it receives
        tokio::spawn(async move {
            while let Ok(msg) = conn.read_datagram().await {
                let txt = String::from_utf8(msg.into())?;
                println!("received: {txt}");
                // Echo back using fire-and-forget semantics
                conn.send_datagram(txt.into_bytes().into())?;
            }
            Ok::<_, Box<dyn std::error::Error>>(())
        });
    }
    Ok(())
}

Key implementation details:

  • conn.read_datagram().await yields a bytes::Bytes payload.
  • conn.send_datagram(payload) sends a reply without guaranteeing delivery or ordering.

Building a Datagram Client

To initiate unreliable messaging, connect to a remote endpoint and use the same datagram methods. This client from iroh/examples/connect-unreliable.rs sends a single message and awaits the reply:

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

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let endpoint = Endpoint::builder(presets::N0)
        .secret_key(SecretKey::generate())
        .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))),
    );

    // Establish a QUIC connection with datagram support
    let conn = endpoint.connect(remote, b"n0/iroh/examples/0").await?;
    
    // Send a single datagram
    conn.send_datagram(b"hello from client".to_vec().into())?;

    // Await the echo reply
    let reply = conn.read_datagram().await?;
    println!("received reply: {}", String::from_utf8(reply.into())?);
    Ok(())
}

The same send_datagram and read_datagram calls function identically whether the path uses direct UDP or requires relay traversal. This transparency simplifies development for mobile or NATed environments.

Datagrams vs QUIC Streams: When to Use Each

Choose the appropriate transport based on your reliability requirements:

Use Case Preferred API
Large ordered payloads with guaranteed delivery QUIC streams (open_bi, open_uni)
Small, latency-critical messages where occasional loss is acceptable Datagrams (send_datagram, read_datagram)
Periodic status updates or sensor data Datagrams
Real-time gaming or VoIP packets Datagrams
Request-reply patterns with single-packet payloads Datagrams

Summary

  • Iroh’s datagram transport leverages QUIC’s unreliable datagram frames for fire-and-forget messaging.
  • Use Connection::send_datagram to emit small payloads and Connection::read_datagram to receive them.
  • The API in iroh/src/endpoint/connection.rs works transparently over direct UDP or relay connections via iroh/src/socket/transports/relay/actor.rs.
  • Payloads must fit within max_datagram_size() (typically a few KB) and may be lost or reordered.
  • Configure the Endpoint with presets::N0 to enable default relay and datagram support.

Frequently Asked Questions

What is the maximum size of a datagram in iroh?

The maximum size is determined by the QUIC connection's MTU and can be retrieved by calling max_datagram_size() on the Connection object. Typically this is a few kilobytes. If you attempt to send a larger payload, send_datagram will return an error. Packet fragmentation is not performed, so you must ensure your data fits within this limit.

Does iroh guarantee delivery of datagrams?

No. Iroh’s datagram transport is unreliable and unordered by design. Datagrams may be lost, duplicated, or arrive out of order. If your application requires guaranteed delivery, use iroh’s QUIC stream API (open_bi, open_uni) instead, which provides reliable, ordered byte streams.

How does iroh handle datagrams when direct UDP is blocked?

When direct UDP communication cannot be established due to NATs or firewalls, iroh automatically routes datagrams through a relay server. The implementation in iroh/src/socket/transports/relay/actor.rs handles this transparently, meaning send_datagram and read_datagram work identically regardless of whether the underlying path is direct UDP or relayed traffic.

Can I use send_datagram_wait to handle congestion?

Yes. While send_datagram returns immediately with an error if the buffer is full, send_datagram_wait waits asynchronously until buffer space is available under congestion control. This is useful when you want to transmit datagrams but need to apply backpressure rather than dropping packets immediately.

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 →