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

> Learn to use irohs datagram transport for fire-and-forget messaging. Leverage QUIC unreliable datagram frames for direct UDP and relay connections without application changes.

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

---

**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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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.

```rust
// 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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/listen-unreliable.rs) demonstrates an echo server:

```rust
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`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/connect-unreliable.rs) sends a single message and awaits the reply:

```rust
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`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) works transparently over direct UDP or relay connections via [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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.