Using Uni-Directional QUIC Streams in iroh: One-Way Communication Patterns
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 defines the entry points for uni-directional stream management.
| Method | Location | Description |
|---|---|---|
Connection::open_uni |
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 |
Accepts the next incoming uni-directional stream. Returns an AcceptUni future that resolves to a RecvStream. |
SendStream::finish |
src/endpoint.rs |
Signals the end of data transmission, closing the sending side gracefully. |
RecvStream::read_to_end |
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.
Implementation Workflow
Client-Side: Opening a Send-Only Stream
Establish the connection via your Endpoint, then open a uni-directional stream to transmit data:
// 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:
// 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:
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. 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), 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:
SendStreamfor the opener,RecvStreamfor the acceptor. - Use
Connection::open_uni()insrc/endpoint/connection.rsto create outgoing streams, andConnection::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’sread_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), 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.
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 →