# Iroh QUIC Stream Implementation: Bidirectional vs Unidirectional Streams Explained

> Explore Iroh's QUIC stream implementation differences between bidirectional and unidirectional streams. Understand how methods like open_bi and open_uni manage one-way or dual-channel communication.

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

---

**Iroh distinguishes between bidirectional and unidirectional QUIC streams through the `open_bi()`, `open_uni()`, `accept_bi()`, and `accept_uni()` methods on `Connection`, where bidirectional streams combine a send and receive channel while unidirectional streams offer only one-way communication.**

The `n0-computer/iroh` repository implements a peer-to-peer networking stack built on QUIC, exposing stream abstractions that allow developers to choose between full-duplex and half-duplex communication patterns. Understanding the differences between these Iroh QUIC stream types is essential for optimizing data transfer in distributed applications.

## Understanding QUIC Stream Types in Iroh

Iroh provides four fundamental operations for stream creation, corresponding to the two directionalities defined in the QUIC specification.

### Bidirectional Streams

**Bidirectional streams** allow both endpoints to send and receive data on the same logical channel. When you call `open_bi()` on a `Connection`, the method returns a tuple of `(SendStream, RecvStream)`, representing the outgoing and incoming halves of the channel.

According to the implementation in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) (lines 862-886), `open_bi()` delegates to the underlying `noq` crate:

```rust
// From iroh/src/endpoint/connection.rs#L862-L886
pub async fn open_bi(&self) -> Result<(SendStream, RecvStream), ConnectionError> {
    let (send, recv) = self.inner.open_bi().await?;
    Ok((SendStream::new(send), RecvStream::new(recv)))
}

```

The corresponding `accept_bi()` method waits for incoming bidirectional streams initiated by the peer. Bidirectional streams are essentially two linked unidirectional streams—one flowing in each direction—exposed as a single logical abstraction.

### Unidirectional Streams

**Unidirectional streams** support data flow in only one direction. The `open_uni()` method creates an outgoing stream where the local side can only send data, while `accept_uni()` accepts an incoming stream where the local side can only receive.

In [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) (lines 74-76), the implementation pattern mirrors the bidirectional approach:

```rust
// Returns SendStream only
let send = conn.open_uni().await?;

```

The peer must explicitly call `accept_uni()` to receive this stream, creating a clear separation between sender and receiver responsibilities.

## Implementation Details in the Iroh Source Code

The stream implementation resides primarily in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs), with type definitions and re-exports located in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs).

Iroh's QUIC layer is deliberately thin; the `Connection` type wraps an inner `noq::Connection` and delegates heavy lifting—such as frame handling, flow control, and congestion control—to the underlying `noq` crate. As implemented in `n0-computer/iroh`, the `open_bi()` method creates a `noq::OpenBi` future, which resolves to a pair of related unidirectional streams that satisfy the QUIC specification's requirement that bidirectional streams act as coordinated pairs.

Both stream types are cheap to create. A single QUIC connection can support up to **2⁶²** total streams (the sum of uni- and bidirectional), effectively providing unlimited capacity for typical workloads (see [`connection.rs`](https://github.com/n0-computer/iroh/blob/main/connection.rs) lines 63-66).

## Practical Code Examples

### Opening a Bidirectional Stream

Use `open_bi()` when you need to send data and receive a response on the same logical channel:

```rust
use iroh::endpoint::Connection;

async fn chat_bidirectional(conn: &Connection) -> Result<(), Box<dyn std::error::Error>> {
    // Initiate bidirectional stream
    let (mut send, mut recv) = conn.open_bi().await?;
    
    // Send request
    send.write_all(b"Hello from client").await?;
    send.finish().await?;
    
    // Read response
    let mut buf = vec![0; 1024];
    let n = recv.read(&mut buf).await?;
    println!("Server replied: {}", String::from_utf8_lossy(&buf[..n]));
    
    Ok(())
}

```

### Sending via Unidirectional Stream

Use `open_uni()` for fire-and-forget patterns where no immediate response is needed:

```rust
async fn send_large_blob(conn: &Connection, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
    // Open unidirectional stream (send-only)
    let mut send = conn.open_uni().await?;
    
    // Stream data without waiting for reply
    send.write_all(data).await?;
    send.finish().await?;
    
    Ok(())
}

```

### Accepting a Unidirectional Stream

The receiving side uses `accept_uni()` to handle incoming one-way data:

```rust
async fn receive_data(conn: &Connection) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    // Wait for incoming unidirectional stream
    let mut recv = conn.accept_uni().await?;
    
    // Read all data into buffer
    let mut data = Vec::new();
    recv.read_to_end(&mut data).await?;
    
    println!("Received {} bytes", data.len());
    Ok(data)
}

```

## When to Use Each Stream Type

Choose your stream type based on your communication pattern:

- **Bidirectional streams** (`open_bi`/`accept_bi`): Use for RPC-style interactions, request-response protocols, or any scenario requiring correlated two-way communication on a single channel. The `SendStream` and `RecvStream` pair maintains ordering and flow control across both directions.

- **Unidirectional streams** (`open_uni`/`accept_uni`): Use for event streaming, file transfers, or telemetry where the sender pushes data without expecting a direct reply. This pattern reduces overhead by avoiding the allocation of a return channel when it is not needed.

## Summary

- **Bidirectional streams** in Iroh provide full-duplex communication through `open_bi()` and `accept_bi()`, returning a `(SendStream, RecvStream)` tuple that maps to a pair of related unidirectional primitives in the underlying `noq` crate.
- **Unidirectional streams** offer half-duplex communication via `open_uni()` (send-only) and `accept_uni()` (receive-only), suitable for unidirectional data pushes.
- The implementation in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) (lines 862-886) delegates to `noq::Connection`, with Iroh providing ergonomic async wrappers around the QUIC protocol.
- Both stream types support up to 2⁶² concurrent streams per connection, making them lightweight enough to create on demand for individual operations.

## Frequently Asked Questions

### What is the difference between `open_bi` and `open_uni` in Iroh?

`open_bi()` creates a bidirectional stream that returns both a `SendStream` and a `RecvStream`, allowing you to write data and read responses on the same logical channel. `open_uni()` creates a unidirectional stream that returns only a `SendStream`, restricting the flow to one direction from the opener to the peer, who must call `accept_uni()` to receive it.

### How many concurrent streams can an Iroh QUIC connection handle?

According to the source code in [`connection.rs`](https://github.com/n0-computer/iroh/blob/main/connection.rs) (lines 63-66), a single QUIC connection can handle up to **2⁶²** total streams (the sum of bidirectional and unidirectional streams). This limit is sufficiently large that typical applications can treat streams as virtually unlimited resources, creating them per operation without concern for connection exhaustion.

### Does Iroh implement the QUIC stream logic directly?

No, Iroh's `Connection` type in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs) provides a thin async wrapper that delegates to the underlying `noq` crate. The heavy lifting—including frame parsing, flow control, and congestion management—is handled by `noq`, while Iroh focuses on providing ergonomic APIs and integrating transport concerns like NAT traversal and path selection.

### When should I use a unidirectional stream instead of a bidirectional one?

Use unidirectional streams when your communication pattern involves only one-way data flow, such as streaming logs, pushing file chunks, or sending heartbeats. Unidirectional streams reduce resource overhead by allocating only one direction of flow. Use bidirectional streams when you need correlated two-way communication, such as RPC calls where the request and response must be paired on the same logical channel.