# How to Integrate iroh with Different Rust Asynchronous Runtimes: Tokio and async-std

> Learn how to integrate iroh with Rust asynchronous runtimes like Tokio and async-std. Discover strategies for embedding runtimes to ensure seamless networking.

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

---

**iroh requires a Tokio runtime to function because it relies on Tokio-specific primitives like `tokio::spawn`, `tokio::time`, and `tokio::sync` channels throughout its networking stack, so you must either run your entire application on Tokio or embed a Tokio runtime within other async executors like async-std.**

iroh is an async-first peer-to-peer networking library that provides QUIC-based connectivity for distributed applications. While powerful for building decentralized systems, it is tightly coupled to Tokio's runtime according to the source code in the n0-computer/iroh repository. This guide explains exactly how to integrate iroh with different Rust asynchronous runtimes based on the actual implementation in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) and related modules.

## Why iroh Requires Tokio

The `iroh` crate declares Tokio as a mandatory dependency in [`iroh/Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/iroh/Cargo.toml), and the core API fundamentally assumes a Tokio executor is present. When you call `Endpoint::builder(...).bind().await` in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs), the implementation implicitly creates a `tokio::runtime::Handle` and spawns background tasks for the QUIC stack, address-lookup services, and relay handling.

Internal modules further enforce this dependency. For example, [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs) directly imports `tokio::sync` channels and `tokio_util::sync::CancellationToken`, which only function correctly when a Tokio executor is running. These Tokio-specific types are woven throughout the networking layer, making it impossible to run iroh on a generic async executor without Tokio present.

## Standard Integration with Tokio

Since iroh is built on Tokio, the simplest and most performant approach is to use Tokio for your entire application. The official examples demonstrate this pattern using the `#[tokio::main]` attribute.

In [`iroh/examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/echo.rs), the canonical implementation looks like this:

```rust
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0)
        .bind()
        .await?;
    
    // Accept connections and handle QUIC streams...
    Ok(())
}

```

This approach works because `#[tokio::main]` initializes a multi-threaded Tokio runtime that drives all iroh tasks, including the internal background workers spawned during `bind()`.

## Using iroh with async-std

If you maintain an existing async-std codebase, you cannot eliminate Tokio, but you can isolate it. You have two idiomatic ways to provide the required Tokio executor while keeping your outer application on async-std.

### Option 1: Explicit Tokio Runtime

Build a dedicated Tokio runtime locally and block on it within your async-std task. This isolates the Tokio executor from async-std while satisfying iroh's requirements.

```rust
use async_std::task;
use iroh::{Endpoint, endpoint::presets};

fn main() {
    task::block_on(async {
        // Build a dedicated Tokio runtime for iroh
        let rt = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .expect("create Tokio runtime");

        // Run iroh inside that runtime
        rt.block_on(async {
            let endpoint = Endpoint::builder(presets::N0)
                .bind()
                .await
                .expect("bind endpoint");
            
            // Use the endpoint (dial, accept, etc.) here
            // ...
        });
    });
}

```

The `rt.block_on` call creates a Tokio executor that drives all iroh tasks, while the outer `async_std::task::block_on` simply waits for the operation to complete.

### Option 2: Using the async_compat Crate

The `async_compat` crate provides a thin wrapper that lets Tokio futures run on any executor. This approach requires less boilerplate but adds a small overhead.

```rust
use async_std::task;
use async_compat::CompatExt; // adds `.compat()` to futures
use iroh::{Endpoint, endpoint::presets};

fn main() {
    task::block_on(async {
        // The future returned by `bind` is a Tokio future
        let endpoint = Endpoint::builder(presets::N0)
            .bind()
            .await
            .compat()
            .await
            .expect("bind endpoint");
        
        // Now you can use `endpoint` inside async-std as usual
        // ...
    });
}

```

The `.compat()` method converts the Tokio-based future into a generic `Future` that async-std can poll. Internally, it spins up a hidden Tokio runtime, so you do not have to manage one manually.

## Key Considerations and Performance Implications

When deciding how to integrate iroh with your existing runtime, consider the following trade-offs:

- **New projects**: Use Tokio (`#[tokio::main]`) directly. Since iroh already depends on Tokio, requiring it as your primary runtime eliminates glue code and potential compatibility issues.

- **Existing async-std codebases**: Use the explicit Tokio runtime method for production systems where you need fine-grained control, or use `async_compat` for rapid prototyping.

- **Mixed executors**: If you must intermix Tokio and async-std tasks, create a multi-threaded Tokio runtime and spawn async-std futures onto it via `tokio::task::spawn_blocking`, or vice versa. This keeps both executors alive while letting each library use its native primitives.

**Critical configuration**: When embedding a Tokio runtime, always call `enable_all()` on the `tokio::runtime::Builder`. Without this flag, iroh's internal timers (`tokio::time`) and I/O utilities will panic at runtime.

**Performance note**: The `async_compat` crate adds a small overhead due to the extra task wrapper. For low-level networking scenarios requiring maximum throughput, the explicit runtime method defined in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) is more robust.

## Summary

- iroh is architecturally dependent on Tokio primitives found in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) and [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs), requiring a Tokio runtime for all operations.
- For Tokio-based applications, use `#[tokio::main]` and follow the patterns in [`iroh/examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/echo.rs) for the cleanest integration.
- For async-std applications, wrap iroh calls in a dedicated Tokio runtime using `tokio::runtime::Builder` or employ the `async_compat` crate to bridge the executors.
- Always configure the Tokio runtime with `enable_all()` to ensure timers and I/O drivers are available for iroh's background tasks.

## Frequently Asked Questions

### Can I use iroh without Tokio?

No, iroh does not expose a `no-tokio` feature in [`iroh/Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/iroh/Cargo.toml). The crate depends on Tokio-specific types like `tokio::sync` channels and `tokio_util::sync::CancellationToken` throughout its internals, including the relay actor implementation in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs). Completely dropping Tokio is not possible with the current architecture.

### What is the performance cost of using async_compat with iroh?

The `async_compat` crate adds a small overhead due to the extra task wrapper that bridges the two executors. While convenient for prototyping, the explicit Tokio runtime method is more robust for production systems handling high-throughput QUIC streams.

### How do I ensure iroh timers work correctly when embedding Tokio in async-std?

When building the Tokio runtime with `tokio::runtime::Builder`, you must call `enable_all()` to ensure the timer and I/O drivers are initialized. Without this configuration, calls to `tokio::time` utilities inside iroh will panic when the code attempts to create timeouts or delays.

### Can I mix Tokio and async-std tasks in the same application with iroh?

Yes, you can create a multi-threaded Tokio runtime and spawn async-std futures onto it using `tokio::task::spawn_blocking`, or run async-std as the outer executor with an embedded Tokio runtime specifically for iroh operations. This approach keeps both executors alive while satisfying iroh's strict runtime requirements.