# How to Implement Unreliable Datagram Connections in iroh

> Learn to implement unreliable datagram connections in iroh using Connection read_datagram and send_datagram. Exchange small unordered packets efficiently over QUIC.

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

---

**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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/listen-unreliable.rs) accepts incoming connections and echoes responses via unreliable datagrams:

```rust
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`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/connect-unreliable.rs) establishes a connection and exchanges a single datagram:

```rust
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`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/listen-unreliable.rs) and [`iroh/examples/connect-unreliable.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/connect-unreliable.rs)
- All datagram APIs are located in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`).