# How to Use Datagrams in iroh QUIC Connections: Unreliable Messaging Guide

> Learn how to use datagrams in iroh QUIC connections. Send and receive small, unreliable packets for low-latency signaling with iroh's Connection type.

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

---

**iroh exposes QUIC application datagrams through the `Connection` type, providing `read_datagram()` for receiving and `send_datagram()` for transmitting small, unreliable packets ideal for low-latency signaling.**

iroh is a peer-to-peer networking library that builds on the QUIC protocol via the `noq` library, offering both reliable streams and unreliable datagrams. While streams guarantee ordered delivery, **datagrams in iroh QUIC connections** provide a lightweight, unordered transport primitive perfect for heartbeats, NAT coordination, and loss-tolerant messages. This guide covers the complete API for sending and receiving datagrams, including buffer management and practical examples from the official repository.

## What Are QUIC Datagrams in iroh?

QUIC supports **application datagrams**—small, unreliable, unordered packets delivered within a single QUIC packet. In iroh, the `Connection` type wraps a `noq::Connection` and exposes this primitive through a simple async API. According to the source code in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) (lines 860–927), the datagram methods forward directly to the underlying QUIC implementation, which handles encryption, fragmentation, and delivery (or discarding if the peer does not support datagrams).

Because datagrams bypass congestion control and ordering guarantees, they may be lost, reordered, or duplicated. They are distinct from the reliable bidirectional (`open_bi`) and unidirectional (`open_uni`) streams also available on the `Connection`.

## The iroh Datagram API

The `Connection` type provides several methods for datagram handling, each serving different flow-control needs.

### Reading Incoming Datagrams

To receive data, use **`read_datagram()`**, which returns a future that yields a `ReadDatagram<'_>` containing the next available datagram:

```rust
while let Ok(msg) = conn.read_datagram().await {
    let text = String::from_utf8(msg.into())?;
    println!("Received: {}", text);
}

```

This method should be called in a loop until the connection closes. Each call awaits the next available packet without allocating a stream.

### Sending Datagrams

For fire-and-forget transmission, use **`send_datagram(data)`**, which immediately queues the data for transmission:

```rust
conn.send_datagram(bytes.into())?;

```

**Important:** If the send buffer is full, this method drops the oldest pending datagram to make room for the new one. For payloads where loss is acceptable—such as periodic heartbeats—this behavior is desirable.

### Handling Back-Pressure

When preserving older datagrams is critical, use **`send_datagram_wait(data)`**. Unlike the non-blocking variant, this returns a future that resolves only once buffer space is available, preventing drops:

```rust
conn.send_datagram_wait(bytes.into()).await?;

```

This approach is safer for control signals where dropping a prior message would break protocol synchronization.

### Checking Size Limits and Buffer Space

Datagrams are limited by the current path MTU. Before sending, check **`max_datagram_size()`** to determine the current per-peer limit (typically a few kilobytes, though it may shrink during path changes):

```rust
let max_size = conn.max_datagram_size();

```

To query available capacity without sending, use **`datagram_send_buffer_space()`**, which reports how many bytes can be sent without causing drops:

```rust
let space = conn.datagram_send_buffer_space();

```

## Complete Working Examples

The iroh repository provides full working examples demonstrating both sides of a datagram conversation.

### Server Example (listen-unreliable.rs)

The listener example in [`iroh/examples/listen-unreliable.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/listen-unreliable.rs) accepts incoming QUIC connections and echoes back any received datagrams:

```rust
use iroh::{Endpoint, RelayMode, SecretKey, endpoint::presets};
use n0_error::Result;

const EXAMPLE_ALPN: &[u8] = b"n0/iroh/examples/0";

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

    endpoint.online().await;

    while let Some(incoming) = endpoint.accept().await {
        let conn = incoming.accept()?.await?;
        
        tokio::spawn(async move {
            while let Ok(msg) = conn.read_datagram().await {
                let txt = String::from_utf8(msg.into()).unwrap();
                println!("received: {txt}");

                let reply = format!("hi! you connected to {}. bye", conn.id());
                conn.send_datagram(reply.into_bytes().into()).unwrap();
            }
        });
    }

    Ok(())
}

```

*Source:* <https://github.com/n0-computer/iroh/blob/main/iroh/examples/listen-unreliable.rs>

### Client Example (connect-unreliable.rs)

The connector example in [`iroh/examples/connect-unreliable.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/connect-unreliable.rs) establishes a connection, sends a single datagram, and awaits the response:

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

const EXAMPLE_ALPN: &[u8] = b"n0/iroh/examples/0";

#[derive(Parser)]
struct Cli {
    #[clap(long)] endpoint_id: iroh::EndpointId,
    #[clap(long, value_parser, num_args = 1.., value_delimiter = ' ')] addrs: Vec<SocketAddr>,
    #[clap(long)] relay_url: iroh::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![EXAMPLE_ALPN.to_vec()])
        .relay_mode(RelayMode::Default)
        .bind()
        .await?;

    endpoint.online().await;

    let remote_addrs = args
        .addrs
        .into_iter()
        .map(TransportAddr::Ip)
        .chain(std::iter::once(TransportAddr::Relay(args.relay_url)));

    let remote = EndpointAddr::from_parts(args.endpoint_id, remote_addrs);
    let conn = endpoint.connect(remote, EXAMPLE_ALPN).await?;

    let payload = format!("{} says hello!", endpoint.id());
    conn.send_datagram(payload.clone().into_bytes().into())?;

    let reply = conn.read_datagram().await?;
    println!("received: {}", String::from_utf8(reply.into()).unwrap());

    Ok(())
}

```

*Source:* <https://github.com/n0-computer/iroh/blob/main/iroh/examples/connect-unreliable.rs>

Both examples use the same ALPN (`b"n0/iroh/examples/0"`), generate temporary `SecretKey`s, and rely on the default relay mode for NAT traversal.

## When to Use Datagrams vs. Streams

Choose the right transport primitive based on reliability requirements:

- **Datagrams** (`read_datagram`, `send_datagram`): Best for low-latency signaling, heartbeats, NAT-punching coordination, or any payload that tolerates loss. They are unordered and may be dropped by the network or buffer management.
- **Streams** (`open_bi`, `open_uni`): Required for reliable, ordered data transfer such as file transfers, RPC requests, or any protocol requiring guaranteed delivery.

## Summary

- The `Connection` type in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) (lines 860–927) exposes QUIC datagram functionality through `read_datagram()`, `send_datagram()`, and `send_datagram_wait()`.
- `read_datagram()` awaits incoming packets; `send_datagram()` transmits immediately (potentially dropping old data); `send_datagram_wait()` preserves ordering by waiting for buffer space.
- Always check `max_datagram_size()` before sending, as the limit varies with path MTU changes.
- Reference [`listen-unreliable.rs`](https://github.com/n0-computer/iroh/blob/main/listen-unreliable.rs) and [`connect-unreliable.rs`](https://github.com/n0-computer/iroh/blob/main/connect-unreliable.rs) for complete client/server implementations using the ALPN `b"n0/iroh/examples/0"`.
- Reserve datagrams for loss-tolerant signaling; use `open_bi` or `open_uni` streams when reliability is required.

## Frequently Asked Questions

### What is the maximum size of a datagram in iroh?

Call `Connection::max_datagram_size()` to determine the current limit, which is typically a few kilobytes but may shrink when the path MTU changes. The underlying QUIC implementation handles fragmentation into single packets, but exceeding this limit will result in transmission failure.

### Can datagrams be lost or reordered?

Yes. Datagrams are explicitly unreliable and unordered. They may be lost in transit, duplicated, or arrive out of order relative to other datagrams or streams. For guaranteed delivery, use the reliable stream API (`open_bi` or `open_uni`).

### How do I prevent datagrams from being dropped when the buffer is full?

Use `send_datagram_wait()` instead of `send_datagram()`. The former returns a future that resolves only when buffer space is available, ensuring older datagrams are not evicted to make room for new ones.

### Where can I find the implementation details?

The high-level API is implemented in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) (lines 860–927), which wraps the `noq::Connection` type. For relay-specific handling (relevant when traversing NATs), see [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs), which manages queuing and forwarding of datagrams to relay servers.