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 inboundbytes::Bytespayloads.
// 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().awaityields abytes::Bytespayload.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_datagramto emit small payloads andConnection::read_datagramto receive them. - The API in
iroh/src/endpoint/connection.rsworks transparently over direct UDP or relay connections viairoh/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
Endpointwithpresets::N0to 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →