# Difference Between Uni-Directional and Bi-Directional QUIC Streams in iroh

> Understand uni-directional vs bi-directional QUIC streams in iroh. Learn how iroh enables one-way data flow or full-duplex communication for your applications.

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

---

**Uni-directional streams in iroh allow only the creator to send data (receive-only for the peer), while bi-directional streams enable full-duplex communication where both endpoints can send and receive on the same logical channel.**

Iroh is a peer-to-peer networking stack built on the Quinn QUIC implementation that exposes two distinct stream primitives through its public API. Understanding the difference between uni-directional and bi-directional QUIC streams in iroh is essential for designing efficient protocols and managing connection resources, as each type maps to specific QUIC transport semantics and memory allocation patterns.

## Core Differences Between Stream Types

### Directionality and Ownership

Uni-directional streams are owned exclusively by the endpoint that creates them. When you call `Connection::open_uni()` in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs), the method forwards to the internal QUIC connection (`self.inner.open_uni()`) and returns a `SendStream` that can only write data. The remote peer receives a read-only handle via `Connection::accept_uni()`. This maps directly to QUIC's unidirectional stream type (type 0).

Bi-directional streams provide full-duplex communication over a single logical channel. Calling `Connection::open_bi()` returns a tuple of `(SendStream, RecvStream)`, allowing both peers to simultaneously send and receive. This corresponds to QUIC's bidirectional stream type (type 1), where the peer accepts the stream via `Connection::accept_bi()` and receives the complementary handles.

### API Methods and Return Types

The architectural differences manifest in the API signatures:

- **`open_uni()`** → Returns `SendStream` (send-only)
- **`open_bi()`** → Returns `(SendStream, RecvStream)` (full-duplex)
- **`accept_uni()`** → Returns `RecvStream` (receive-only)
- **`accept_bi()`** → Returns `(SendStream, RecvStream)` (full-duplex)

Because a uni-directional stream cannot be read from by the creator, it avoids the overhead of allocating a receive buffer on the sending side, reducing memory usage for one-way traffic.

## Configuration and Resource Limits

Both stream types are subject to concurrent limits configured via `QuicTransportConfigBuilder` in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs):

- **`max_concurrent_uni_streams`** sets the maximum number of concurrent uni-directional streams allowed per connection
- **`max_concurrent_bidi_streams`** sets the maximum number of concurrent bi-directional streams allowed per connection

These limits are enforced by the underlying QUIC layer to prevent resource exhaustion. Flow control applies uniformly to both stream types through `stream_receive_window` and the global `receive_window` settings.

## Practical Implementation Examples

The following example demonstrates both patterns using iroh's endpoint API:

```rust
use iroh::endpoint::{Endpoint, QuicTransportConfigBuilder};
use quinn::VarInt;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Configure transport limits (optional)
    let transport_cfg = QuicTransportConfigBuilder::default()
        .max_concurrent_uni_streams(VarInt::from_u32(100))
        .max_concurrent_bidi_streams(VarInt::from_u32(20))
        .build();

    let endpoint = Endpoint::bind().await?;
    
    // ---------------------------------------------------------
    // Uni-directional: One-way data push (see remote-info.rs)
    // ---------------------------------------------------------
    let conn = endpoint.connect(remote_addr, b"my-alpn").await?;
    let mut send = conn.open_uni().await?;  // Returns SendStream only
    send.write_all(b"fire-and-forget payload").await?;
    send.finish()?;  // Close the sending side
    // Peer receives this via accept_uni()

    // ---------------------------------------------------------
    // Bi-directional: Request/response pattern (see echo.rs)
    // ---------------------------------------------------------
    let conn = endpoint.connect(remote_addr, b"my-alpn").await?;
    let (mut send, mut recv) = conn.open_bi().await?;  // Full-duplex pair
    
    send.write_all(b"request").await?;
    send.finish()?;
    
    let mut response = Vec::new();
    recv.read_to_end(&mut response).await?;
    
    Ok(())
}

```

The uni-directional pattern mirrors the implementation in [`iroh/examples/remote-info.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/remote-info.rs), while the bi-directional example follows the echo protocol demonstrated in [`iroh/examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/echo.rs).

## When to Use Each Stream Type

**Uni-directional streams** are optimal for simple one-way data pushes such as fire-and-forget notifications, telemetry uploads, or logging streams. By eliminating the receive buffer on the sender side, they reduce memory overhead and simplify error handling for unidirectional flows.

**Bi-directional streams** are necessary for interactive protocols where both peers must exchange data, such as request/response cycles, echo services, or full-duplex communication channels where immediate feedback is required.

## Summary

- **Uni-directional streams** (`open_uni`/`accept_uni`) provide single-direction communication with **send-only** handles for the creator and **receive-only** handles for the peer, mapping to QUIC type 0 streams.
- **Bi-directional streams** (`open_bi`/`accept_bi`) provide **full-duplex** communication via `(SendStream, RecvStream)` tuples, mapping to QUIC type 1 streams.
- Resource limits are configured separately via `QuicTransportConfigBuilder::max_concurrent_uni_streams` and `max_concurrent_bidi_streams` in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs).
- Uni-directional streams reduce memory overhead by avoiding receive buffer allocation on the sender, while bi-directional streams are required for interactive protocols.

## Frequently Asked Questions

### Can I convert a uni-directional stream into a bi-directional stream in iroh?

No, stream directionality is fixed at creation time by the QUIC protocol specification. If you need bidirectional communication, you must open a bi-directional stream using `Connection::open_bi()`. While you could open two uni-directional streams (one in each direction), this does not provide the same atomic guarantees or flow control as a single bi-directional stream.

### How do I choose between uni-directional and bi-directional streams for my protocol?

Choose **uni-directional** when you need simplex communication such as event streaming, file uploads, or heartbeat messages where the sender never needs to read a response. Choose **bi-directional** when your protocol requires request/response pairs, interactive sessions, or any scenario where both peers need to transmit data concurrently over the same logical channel.

### What happens if I exceed the configured stream limits in iroh?

If you attempt to open more streams than configured in `max_concurrent_uni_streams` or `max_concurrent_bidi_streams`, the QUIC layer will block the operation or return an error until existing streams are closed. These limits prevent memory exhaustion and are enforced internally by the Quinn QUIC implementation before the application layer receives the stream handle.

### Do flow control settings apply differently to uni-directional vs bi-directional streams?

No, flow control settings apply uniformly to both types. The `stream_receive_window` and global `receive_window` configured in `QuicTransportConfigBuilder` govern how much data can be buffered for incoming data regardless of whether the stream is uni-directional or bi-directional. The key difference is that uni-directional streams only allocate receive buffers on the receiving endpoint, while bi-directional streams maintain receive buffers on both ends.