# Using Uni-Directional QUIC Streams in iroh: One-Way Communication Patterns

> Learn how to use uni-directional QUIC streams in iroh for efficient fire-and-forget or request-only messaging. Simplify your one-way communication patterns.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: deep-dive
- Published: 2026-06-21

---

**Uni-directional QUIC streams in iroh provide single-direction communication channels where `Connection::open_uni()` yields a `SendStream` for the client and `Connection::accept_uni()` yields a `RecvStream` for the server, enabling efficient fire-and-forget or request-only messaging without duplex coordination overhead.**

iroh’s networking layer is built on QUIC via the `noq` crate, exposing both bi-directional and **uni-directional streams**. Unlike bi-directional streams that provide read-write pairs, uni-directional streams offer a leaner abstraction: the initiating peer gets exclusive write access while the receiving peer gets exclusive read access. This pattern is ideal for telemetry, logging, or client-to-server uploads where the response is handled separately or unnecesary.

## Core API for Uni-Directional Streams

The `Connection` type in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs) defines the entry points for uni-directional stream management.

| Method | Location | Description |
|--------|----------|-------------|
| `Connection::open_uni` | [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs) | Initiates an outgoing uni-directional stream. Returns an `OpenUni` future that resolves to a `SendStream`. |
| `Connection::accept_uni` | [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs) | Accepts the next incoming uni-directional stream. Returns an `AcceptUni` future that resolves to a `RecvStream`. |
| `SendStream::finish` | [`src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint.rs) | Signals the end of data transmission, closing the sending side gracefully. |
| `RecvStream::read_to_end` | [`src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint.rs) | Consumes the stream until EOF, buffering all received data into a `Vec<u8>`. |

Both methods are **async** and return futures that must be awaited. Uni-directional streams inherit the underlying QUIC connection’s flow control, congestion control, and zero-RTT session resumption guarantees.

## Critical Constraints and Deadlock Prevention

The peer that calls `open_uni` **must write** to its `SendStream` before the remote peer can successfully await the corresponding `RecvStream`. 

Calling `open_uni` and then immediately awaiting the receive side without writing will deadlock, as the stream ID is not actually allocated until data flows. This restriction is documented in the comment block surrounding `open_uni` and `accept_uni` in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs).

## Implementation Workflow

### Client-Side: Opening a Send-Only Stream

Establish the connection via your `Endpoint`, then open a uni-directional stream to transmit data:

```rust
// Establish connection (outgoing)
let conn = endpoint.connect(addr, server_id).await?;

// Open uni-directional stream - returns SendStream only
let mut send = conn.open_uni().await?;

// Transmit data and signal EOF
send.write_all(b"telemetry data").await?;
send.finish()?;  // Signals end-of-data to the remote RecvStream

```

### Server-Side: Accepting a Receive-Only Stream

On the server, accept the incoming stream within your protocol handler and consume the data:

```rust
// Accept incoming connection via ProtocolHandler
let conn = acceptor.accept().await?;

// Accept the uni-directional stream - returns RecvStream only
let mut recv = conn.accept_uni().await?;

// Read all data until the client calls finish()
let mut buffer = Vec::new();
recv.read_to_end(&mut buffer).await?;

```

### Connection Lifecycle

Close the underlying QUIC connection when all streams are complete:

```rust
conn.close(quic::VarInt::from_u32(0), b"stream complete");
conn.closed().await;

```

## How QUIC is Wired into iroh

The uni-directional stream API is exposed by `iroh::endpoint::Connection` objects in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs). These are thin wrappers around `noq::Connection` that map QUIC’s native stream types to Rust’s async I/O patterns.

When you configure an `Endpoint` with `Options::quic_config` (defined in [`src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint.rs)), iroh instantiates a `QuicClientConfig`/`QuicServerConfig`. The resulting connection objects expose `open_uni` and `accept_uni` as ergonomic helpers over the raw QUIC transport, handling stream ID allocation and backpressure automatically.

## Summary

- **Uni-directional streams** provide one-way communication: `SendStream` for the opener, `RecvStream` for the acceptor.
- Use `Connection::open_uni()` in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs) to create outgoing streams, and `Connection::accept_uni()` to handle incoming streams.
- **Always write** after opening before the remote can read; otherwise, the stream handshake deadlocks.
- Call `SendStream::finish()` to signal EOF and allow the receiver’s `read_to_end()` to complete.
- Uni-directional streams reduce overhead when you only need simplex communication, such as metrics ingestion or file uploads.

## Frequently Asked Questions

### What is the difference between uni-directional and bi-directional streams in iroh?

**Bi-directional streams** (`open_bi`/`accept_bi`) return a `(SendStream, RecvStream)` tuple, enabling full-duplex communication over a single stream ID. **Uni-directional streams** (`open_uni`/`accept_uni`) return only a `SendStream` or `RecvStream` respectively, enforcing one-way data flow. Choose uni-directional when the client only sends or the server only receives, as it reduces complexity and resource usage.

### Can I mix uni-directional and bi-directional streams on the same connection?

Yes. A single QUIC connection in iroh supports multiplexing both stream types simultaneously. You can call `open_uni()` for fire-and-forget messages and `open_bi()` for request-response interactions over the same `Connection` object without blocking each other.

### Why does my `accept_uni()` hang indefinitely?

The most common cause is the opener failing to write data after calling `open_uni()`. In iroh’s QUIC implementation (as documented in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs)), the stream is not fully established until the sender transmits the first bytes. Ensure the client writes to the `SendStream` immediately after opening it.

### How do I handle backpressure on uni-directional streams?

`SendStream::write_all` and `RecvStream::read` automatically respect QUIC’s flow control windows. If the receiver is slow, `write_all` will yield to the async runtime until buffer space becomes available. For explicit handling, use `SendStream::write` which returns the number of bytes written, allowing you to manage partial writes and buffering strategies manually.