# Using Bi-Directional QUIC Streams in iroh: Implementation Guide

> Learn how to implement bi-directional QUIC streams in iroh for full-duplex communication. Access SendStream and RecvStream using Connection::open_bi and Connection::accept_bi.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-06-21

---

**Bi-directional QUIC streams in iroh provide full-duplex communication channels where each peer holds a `SendStream` and `RecvStream`, accessed via `Connection::open_bi` and `Connection::accept_bi` in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs).**

iroh is a peer-to-peer networking library built on the QUIC protocol (via the `noq` crate) that abstracts complex connection handling into ergonomic async APIs. Working with bi-directional QUIC streams allows you to establish simultaneous read and write channels over a single connection without head-of-line blocking. This guide explores the core API methods, critical deadlock warnings, and implementation patterns found in the n0-computer/iroh repository.

## Core API for Bi-Directional QUIC Streams

The primary interface for bi-directional stream management resides in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs). The `Connection` type exposes two essential async methods for stream establishment that return futures resolving to `(SendStream, RecvStream)` tuples.

### Initiating Outgoing Streams with `open_bi`

To create a new bi-directional stream from the client side, call `Connection::open_bi`. This method returns an `OpenBi` future that resolves to a pair of streams for full-duplex communication.

```rust
let (mut send, mut recv) = conn.open_bi().await?;

```

### Accepting Incoming Streams with `accept_bi`

On the server side, `Connection::accept_bi` waits for the next incoming bi-directional stream request. This method returns an `AcceptBi` future with the same stream tuple structure.

```rust
let (mut send, mut recv) = conn.accept_bi().await?;

```

## Critical Deadlock Warning

A crucial implementation detail in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs) dictates that **the initiating peer must write to its `SendStream` before the remote peer can successfully read from its corresponding `RecvStream`**. Calling `open_bi` and immediately awaiting the receive operation without writing data will deadlock the connection. This behavior stems from QUIC's stream flow-control handshake requirements and is explicitly documented in the source comments surrounding these methods.

## Managing Stream Lifecycle

Proper cleanup ensures reliable QUIC connection management and prevents resource leaks.

### Closing Send Streams with `finish`

Signal end-of-data by calling `SendStream::finish`, defined in [`src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint.rs). This notifies the remote receiver that no more data will be sent.

```rust
send.write_all(b"request data").await?;
send.finish()?; // Signals EOF to remote receiver

```

### Monitoring Connection Termination

Await complete connection shutdown using `Connection::closed`, also located in [`src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint.rs):

```rust
conn.close(quic::VarInt::from_u32(0), b"normal shutdown");
conn.closed().await;

```

## Practical Implementation Examples

### Client-Side Stream Pattern

The client implementation in [`examples/search.rs`](https://github.com/n0-computer/iroh/blob/main/examples/search.rs) demonstrates the complete workflow: open, write, finish, then read. This pattern ensures the deadlock rule is satisfied by writing immediately after opening.

```rust
let (mut send, mut recv) = conn.open_bi().await?;
send.write_all(b"search query").await?;
send.finish()?;

let mut response = Vec::new();
recv.read_to_end(&mut response).await?;

```

### Server-Side Stream Pattern

The echo server in [`examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/examples/echo.rs) shows the acceptance and echo pattern using `tokio::io::copy` to stream data between the receive and send sides.

```rust
let (mut send, mut recv) = conn.accept_bi().await?;
tokio::io::copy(&mut recv, &mut send).await?;
send.finish()?;

```

## QUIC Integration in iroh's Architecture

The QUIC transport layer is configured through `iroh::endpoint::Endpoint` in [`src/lib.rs`](https://github.com/n0-computer/iroh/blob/main/src/lib.rs). When providing `Options::quic_config`, iroh constructs `QuicClientConfig` and `QuicServerConfig` instances that power the underlying `noq` crate connections. The `Connection` type serves as a thin wrapper around `noq::Connection`, exposing the `open_bi` and `accept_bi` helpers while inheriting QUIC's built-in flow-control, congestion control, and loss-recovery guarantees.

## Summary

- **Bi-directional streams** provide full-duplex communication via `SendStream` and `RecvStream` pairs managed in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs).
- **Deadlock prevention** requires the initiator to write data before the remote can receive, as documented in the source comments.
- **API entry points** are `Connection::open_bi` for clients and `Connection::accept_bi` for servers.
- **Lifecycle management** uses `SendStream::finish` to signal EOF and `Connection::closed` to await termination.
- **Real-world examples** are available in [`examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/examples/echo.rs) and [`examples/search.rs`](https://github.com/n0-computer/iroh/blob/main/examples/search.rs) within the n0-computer/iroh repository.

## Frequently Asked Questions

### What is the difference between `open_bi` and `accept_bi` in iroh?

`open_bi` initiates a new outgoing bi-directional stream from the client side, while `accept_bi` waits for and accepts incoming stream requests on the server side. Both methods return a tuple of `(SendStream, RecvStream)` but are used by opposite connection roles according to the implementation in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs).

### Why does my iroh application deadlock when using bi-directional streams?

Deadlocks occur when the initiating peer calls `open_bi` but fails to write data to the `SendStream` before the remote peer attempts to read. The source code in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs) explicitly documents this requirement: you must write before the remote can successfully await the receive stream, as the stream establishment requires data transmission to complete the handshake.

### How do I properly close a bi-directional QUIC stream in iroh?

Call `SendStream::finish` to signal end-of-file to the remote peer, allowing the receiving side to exit its read loop. Once all streams are closed, use `Connection::close` followed by `Connection::closed` to gracefully terminate the underlying QUIC connection and await complete shutdown.

### Where can I find working examples of bi-directional stream usage?

The n0-computer/iroh repository contains [`examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/examples/echo.rs), which demonstrates a server echoing data back over bi-directional streams, and [`examples/search.rs`](https://github.com/n0-computer/iroh/blob/main/examples/search.rs), which shows a client sending requests and reading responses. Both files illustrate proper open-write-finish-read patterns and are referenced in the crate-level documentation in [`src/lib.rs`](https://github.com/n0-computer/iroh/blob/main/src/lib.rs).