How iroh Bi-Directional QUIC Streams Differ from Uni-Directional: A Complete Guide

Uni-directional streams allow only the creator to send data (one-way), while bi-directional streams enable full-duplex communication where both endpoints can send and receive over the same logical channel.

Iroh, developed by n0-computer, builds on the quinn QUIC implementation to provide robust peer-to-peer networking. Understanding how iroh bi-directional QUIC streams differ from their uni-directional counterparts is essential for optimizing memory usage and designing efficient protocols. This guide examines the API differences, configuration options, and architectural implications directly from the source code.

Core Differences Between Stream Types

Iroh exposes two distinct stream types through its public API, each mapping to different QUIC stream types and use cases.

Aspect Uni-Directional Stream Bi-Directional Stream
Directionality Only the endpoint that creates the stream can send data; the remote side can only receive. Both endpoints can send and receive on the same logical stream (full-duplex).
Underlying QUIC Maps to QUIC uni-directional stream (type 0). Maps to QUIC bidirectional stream (type 1).
API Return Type SendStream (write-only). Tuple (SendStream, RecvStream) for simultaneous read/write.
Primary Use Case Fire-and-forget notifications, one-way uploads, logging. Request/response protocols, interactive sessions, echo services.

API Implementation in iroh

The stream creation methods are implemented in iroh/src/endpoint/connection.rs, providing distinct interfaces for each stream type.

Opening Uni-Directional Streams

The Connection::open_uni() method creates a write-only stream handle. Internally, this forwards to self.inner.open_uni() within the QUIC implementation, creating a stream that can only be written to by the creator.

// Connect to remote endpoint
let conn = endpoint.connect(remote_addr, b"my-alpn").await?;

// open_uni returns a SendStream that can only write
let mut send = conn.open_uni().await?;
send.write_all(b"fire-and-forget payload").await?;
send.finish()?;  // Close the sending side

The peer receives this stream through Connection::accept_uni(), which returns a read-only handle corresponding to the SendStream created by the opener.

Opening Bi-Directional Streams

For full-duplex communication, Connection::open_bi() returns both a sender and receiver, allowing simultaneous data flow in both directions.

// open_bi returns both SendStream and RecvStream
let (mut send, mut recv) = conn.open_bi().await?;

// Send request
send.write_all(b"request data").await?;
send.finish()?;  // Signal end of request

// Read response on same stream
let mut response = Vec::new();
recv.read_to_end(&mut response).await?;

The peer receives this pair through Connection::accept_bi(), enabling immediate bidirectional communication without opening a separate stream.

Configuration and Resource Limits

Stream limits are configured via QuicTransportConfigBuilder in iroh/src/endpoint/quic.rs. These settings prevent resource exhaustion by controlling concurrency at the QUIC layer.

Concurrent Stream Limits

  • max_concurrent_uni_streams: Controls how many uni-directional streams can exist simultaneously
  • max_concurrent_bidi_streams: Controls the limit for bi-directional streams
use iroh::endpoint::QuicTransportConfigBuilder;

let transport_cfg = QuicTransportConfigBuilder::default()
    .max_concurrent_uni_streams(VarInt::from_u32(100))
    .max_concurrent_bidi_streams(VarInt::from_u32(20))
    .build();

Flow Control Windows

Both stream types respect the stream_receive_window setting defined in QuicTransportConfigBuilder. This window size applies to the receive buffer allocation for individual streams, while the global receive_window governs the entire connection. These flow control mechanisms apply identically to uni- and bi-directional streams.

Memory and Performance Considerations

Uni-directional streams avoid allocating receive buffers on the sending side because the creator cannot read from them. This reduces memory overhead for one-way traffic patterns like telemetry or logging, where responses are unnecessary.

Bi-directional streams require buffer allocation for both directions but eliminate the overhead of opening separate streams for request/response patterns. For protocols requiring acknowledgment or conversation, they reduce latency by avoiding the stream establishment overhead of two uni-directional channels.

Practical Examples from the Repository

The iroh/examples/remote-info.rs file demonstrates the uni-directional pattern using conn.open_uni().await to push information without awaiting a response.

For bi-directional patterns, the echo protocol implementation (referenced in iroh/examples/echo.rs) shows how the same stream handles both request and response, matching the pattern shown in the repository's README for interactive QUIC protocols.

Summary

  • Uni-directional streams (open_uni/accept_uni) provide one-way write access from creator to peer, mapping to QUIC type 0 streams and reducing memory footprint for send-only operations.
  • Bi-directional streams (open_bi/accept_bi) return (SendStream, RecvStream) tuples enabling full-duplex communication over QUIC type 1 streams, essential for request/response patterns.
  • Configuration happens through QuicTransportConfigBuilder methods max_concurrent_uni_streams and max_concurrent_bidi_streams in iroh/src/endpoint/quic.rs.
  • Resource efficiency favors uni-directional streams for one-way data pushes, while bi-directional streams optimize interactive protocols.

Frequently Asked Questions

When should I use uni-directional streams vs bi-directional in iroh?

Use uni-directional streams when you need simplex communication such as fire-and-forget notifications, log streaming, or one-way file uploads where the sender requires no acknowledgment. Use bi-directional streams for interactive protocols requiring request/response cycles, such as RPC calls, chat messages, or command/control interfaces where both peers must exchange data.

How do I configure stream limits in iroh?

Configure limits through QuicTransportConfigBuilder before building your endpoint. Call max_concurrent_uni_streams() and max_concurrent_bidi_streams() with appropriate VarInt values to set hard limits enforced by the QUIC layer. These settings prevent resource exhaustion by rejecting new stream creation attempts beyond the configured thresholds.

Can I convert a uni-directional stream to bi-directional?

No, stream directionality is fixed at creation time in QUIC. A uni-directional stream (type 0) cannot be upgraded to bi-directional. If you need bidirectional communication after opening a uni-directional stream, you must either open a second uni-directional stream in the opposite direction or open a new bi-directional stream using open_bi().

What happens if I exceed the concurrent stream limits?

The QUIC implementation enforces these limits at the protocol level. When max_concurrent_uni_streams or max_concurrent_bidi_streams is reached, subsequent calls to open_uni() or open_bi() will block or return an error depending on the specific QUIC configuration and async runtime behavior, preventing memory exhaustion and maintaining connection stability.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →