Difference Between Uni-Directional and Bi-Directional QUIC Streams in iroh
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, 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()→ ReturnsSendStream(send-only)open_bi()→ Returns(SendStream, RecvStream)(full-duplex)accept_uni()→ ReturnsRecvStream(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:
max_concurrent_uni_streamssets the maximum number of concurrent uni-directional streams allowed per connectionmax_concurrent_bidi_streamssets 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:
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, while the bi-directional example follows the echo protocol demonstrated in 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_streamsandmax_concurrent_bidi_streamsiniroh/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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →