# How iroh Leverages QUIC to Support Multiple Concurrent Streams: A Deep Dive into the Rust Implementation

> Discover how iroh uses native QUIC stream multiplexing via the noq crate and high concurrency settings for parallel discovery and data transfer on a single UDP connection.

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

---

**iroh uses QUIC's native stream multiplexing capabilities through the noq crate, configuring high concurrency limits via `QuicTransportConfigBuilder` to enable parallel address discovery, NAT traversal, and data transfer over a single UDP connection.**

The n0-computer/iroh repository implements a peer-to-peer networking stack that treats QUIC as a primitive multiplexing layer. By leveraging the *noq* crate's transport configuration, iroh enables multiple logical channels to operate simultaneously across a single connection, supporting the patchbay for NAT traversal, the relay for address discovery, and file-transfer protocols concurrently.

## The Foundation: QUIC Stream Multiplexing in iroh

QUIC natively supports multiplexing independent streams over a single connection, eliminating head-of-line blocking inherent in TCP. In iroh, this capability serves as the backbone for concurrent operations ranging from address discovery to file transfers.

### The Role of the noq Crate

iroh builds its networking layer on top of the **noq** crate, which provides the underlying QUIC implementation. This abstraction allows iroh to configure stream behavior through high-level builders while maintaining direct access to stream concurrency controls.

## Configuring Concurrent Stream Limits

The `QuicTransportConfigBuilder` struct in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs) exposes the primary interface for tuning stream capacity. This builder sets the upper bounds for simultaneously open streams before the endpoint initializes.

### Bidirectional and Unidirectional Stream Configuration

The builder defines two critical parameters:

- **`max_concurrent_bidi_streams`**: Controls bidirectional streams where both endpoints can send and receive data
- **`max_concurrent_uni_streams`**: Controls unidirectional streams for one-way communication

According to the source in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs) (lines 70-84), these values are stored as `VarInt` types and passed directly to the underlying QUIC transport:

```rust
use iroh::endpoint::QuicTransportConfig;

// Configure generous limits for high-concurrency scenarios
let transport_cfg = QuicTransportConfig::builder()
    .max_concurrent_bidi_streams(VarInt::from_u32(100))
    .max_concurrent_uni_streams(VarInt::from_u32(100))
    .send_observed_address_reports(true)  // Enable address discovery frames
    .build();

```

The default builder already sets generous limits appropriate for iroh's multipath hole-punching scenarios, as seen in lines 151-162 of the same file.

## Endpoint Architecture and Connection Handling

Once configured, the `QuicTransportConfig` transforms into a `noq::TransportConfig` wrapped in an `Arc`. This configuration drives the `Endpoint` struct defined in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs), which represents the QUIC endpoint capable of accepting or initiating connections.

### Client and Server Initialization

The endpoint creation process differs slightly between client and server modes, but both apply the transport configuration during initialization:

```rust
use iroh::endpoint::{Endpoint, QuicTransportConfig};

// Client endpoint
let client = Endpoint::client(transport_cfg).await?;
let conn = client.connect(server_addr).await?;

// Server endpoint (binds to specific address)
let server = Endpoint::server(transport_cfg, bind_addr).await?;

```

### Opening Multiple Streams on a Single Connection

The `Connection` type exposes `open_bi()` and `open_uni()` methods for creating new streams. Because the transport config permits many concurrent streams, multiple logical operations can execute side-by-side without blocking:

```rust
// Open several streams in parallel over one connection
let mut handles = Vec::new();
for i in 0..5 {
    let send = conn.open_uni().await?;
    handles.push(tokio::spawn(async move {
        send.write_all(format!("control_msg {}", i).as_bytes()).await?;
        send.finish().await?;
        Ok::<_, std::io::Error>(())
    }));
}

```

Each stream receives a unique stream ID from the underlying QUIC stack, with `SendStream` and `RecvStream` handles enabling independent operation.

## Real-World Usage in iroh-relay

The iroh-relay component demonstrates practical application of these concurrency features. Located in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs) (lines 106-112), the server initialization disables fixed stream limits to allow dynamic allocation:

```rust
use iroh::endpoint::QuicTransportConfig;

let transport_cfg = QuicTransportConfig::builder()
    .max_concurrent_bidi_streams(0_u8.into())   // No fixed limit
    .max_concurrent_uni_streams(0_u8.into())    // Allow any number
    .send_observed_address_reports(true)
    .build();

let server = Endpoint::server(transport_cfg, bind_addr).await?;

```

This configuration enables the relay to handle multiple simultaneous operations per connection:

1. **Address Discovery**: Streams carrying observed address report frames to learn external addresses
2. **NAT Traversal**: Hole-punching streams for establishing direct peer connections  
3. **Application Data**: Separate bidirectional streams for file transfers and queries

The server spawns per-connection handlers that can open unlimited streams for these varied purposes, as implemented in the relay's accept loop.

## Summary

- **iroh uses the noq crate** to provide QUIC functionality, exposing stream controls through `QuicTransportConfigBuilder` in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs)
- **Concurrent stream limits** are configured via `max_concurrent_bidi_streams` and `max_concurrent_uni_streams`, accepting `VarInt` values to set capacity
- **Single connections support multiple streams** through `Connection::open_bi()` and `Connection::open_uni()`, enabling parallel channels for discovery, NAT traversal, and data transfer
- **iroh-relay disables fixed limits** by setting these values to `0_u8.into()`, allowing unlimited dynamic stream creation for relay operations
- **All operations multiplex over one UDP socket**, with QUIC handling stream isolation and congestion control independently

## Frequently Asked Questions

### What is the difference between bidirectional and unidirectional streams in iroh?

Bidirectional streams allow both endpoints to send and receive data simultaneously, making them suitable for request-response protocols. Unidirectional streams permit data flow in one direction only, which iroh uses for control messages and address discovery frames. The `QuicTransportConfigBuilder` sets independent limits for each type via `max_concurrent_bidi_streams` and `max_concurrent_uni_streams`.

### How does iroh handle stream limits compared to default QUIC implementations?

While default QUIC implementations often impose conservative stream limits, iroh configures generous defaults appropriate for peer-to-peer networking. In the iroh-relay server specifically, iroh disables fixed limits entirely by passing `0_u8.into()` to both stream configuration methods, allowing the endpoint to open any number of streams needed for NAT traversal and address discovery.

### Can iroh run multiple protocols over a single QUIC connection?

Yes. iroh's architecture treats QUIC as a multiplexing primitive, running the patchbay for NAT traversal, the relay for address discovery, and file-transfer protocols concurrently over a single connection. Each protocol operates on separate streams opened via `open_bi()` or `open_uni()`, preventing interference between different logical operations.

### What crate does iroh use for its QUIC implementation?

iroh uses the **noq** crate for its underlying QUIC transport. This dependency provides the `TransportConfig` and endpoint primitives that iroh wraps with its `QuicTransportConfigBuilder` and `Endpoint` types, as seen in [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs) and [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs).