# How to Properly Shut Down an Iroh Endpoint and Clean Up Resources

> Learn how to properly shut down an iroh endpoint and clean up resources. Follow best practices to ensure graceful termination and resource release for your n0-computer/iroh projects.

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

---

**Call `Endpoint::close().await` to gracefully shut down an iroh endpoint, then await `Endpoint::closed()` to ensure all background tasks and UDP sockets are fully released before dropping the endpoint.**

The iroh library provides a robust QUIC-based peer-to-peer networking stack, but improper shutdown can leave connections hanging and cause abrupt "connection lost" errors on remote peers. Understanding the **iroh endpoint shutdown and resource cleanup** lifecycle ensures your application terminates cleanly without leaking sockets or orphaning background tasks.

## Why Graceful Shutdown Matters in Iroh

An iroh `Endpoint` represents your local node in the network. It manages UDP sockets, relay connections, NAT traversal helpers, address discovery services, and active QUIC connections. When you drop an endpoint without closing it properly, open connections abort after a timeout, appearing as failures to remote peers.

The source code in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) documents this explicitly: the `close` method comment (lines 1681-1689) warns that dropping without awaiting close causes connections to terminate uncleanly.

## Step 1: Initiate Shutdown with `Endpoint::close()`

The `Endpoint::close()` method is your entry point for graceful shutdown. Located at lines 1703-1705 in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs), this async method forwards to `EndpointInner::close` and waits for the QUIC endpoint to finish draining.

```rust
// Gracefully close the endpoint
ep.close().await;

```

What happens internally:

- **QUIC connection draining**: The method waits for ACKs that confirm remote peers received close notifications. The default timeout is approximately 3 seconds on poor networks.
- **Cancellation token trigger**: `EndpointInner` owns a `ShutdownState` containing a cancellation token. Calling `close` triggers this token, signaling all background tasks to stop.

## Step 2: Monitor Completion with `Endpoint::closed()`

After calling `close()`, use `Endpoint::closed()` to receive an `EndpointClosed` future that resolves when shutdown is fully complete. This future is defined at lines 1812-1825 in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs).

```rust
// Wait for complete shutdown
ep.closed().await;

```

The `EndpointClosed` future provides additional utility through `run_until`, which automatically cancels a task when the endpoint closes:

```rust
use iroh::Endpoint;

// Run work until the endpoint shuts down
ep.closed().run_until(async {
    // Your async work here
    process_data().await
}).await;

```

## Step 3: Background Task Cancellation

The actual shutdown orchestration happens in `EndpointInner::close` within [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs). This internal method coordinates termination of:

- **Socket and transport layers**: The UDP socket and all transport implementations
- **Address lookup services**: DNS and discovery actors
- **NAT traversal helpers**: NAT-PMP, PCP, and UPnP mapping cleanup
- **Net-report task**: Network capability reporting
- **Relay connections**: Active relay transports via `close_all_active_relays` in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs)

The `ShutdownState` struct manages a `tokio_util::sync::CancellationToken` that all background tasks monitor. When triggered, tasks exit their loops and release resources.

## Step 4: UDP Socket Release

The underlying UDP socket remains alive while any clone of the `Endpoint` exists. According to the comment at lines 1712-1718 in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs), the socket is finally released only after:

1. `close()` has been called and completed
2. All clones of the `Endpoint` have been dropped
3. The `Drop` implementation of `EndpointInner` executes

## Complete Shutdown Example

Here is a production-ready pattern for shutting down an iroh endpoint:

```rust
use iroh::{Endpoint, endpoint::presets};
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() -> n0_error::Result<()> {
    // Build and bind the endpoint
    let ep = Endpoint::builder(presets::N0)
        .alpns(vec![b"my-alpn".to_vec()])
        .bind()
        .await?;

    // ... use endpoint for P2P connections, transfers, etc.

    // 1. Gracefully close all QUIC connections
    ep.close().await;

    // 2. Wait for background tasks and socket cleanup
    ep.closed().await;

    // 3. Endpoint is now safe to drop
    drop(ep);
    
    Ok(())
}

```

## Pattern for Long-Running Applications

For services that need to respond to shutdown signals while monitoring the endpoint:

```rust
use iroh::Endpoint;

async fn run_service(ep: Endpoint) {
    // Spawn task that stops when endpoint closes
    let monitor = tokio::spawn({
        let closed = ep.closed();
        async move {
            closed.run_until(async {
                // Background work here
                loop {
                    tokio::time::sleep(Duration::from_secs(1)).await;
                    println!("Working...");
                }
            }).await;
            println!("Stopped because endpoint closed");
        }
    });

    // Later, trigger shutdown
    ep.close().await;
    
    // Wait for monitor to finish
    monitor.await.unwrap();
}

```

## Per-Connection Graceful Close

Individual connections support graceful termination via `Connection::close` in [`iroh/src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/connection.rs). This sends a QUIC close frame to the remote peer with an error code and reason string, allowing the remote application to distinguish intentional closure from network failure.

```rust
// Close a specific connection gracefully
connection.close(0u32.into(), b"done");

```

## Summary

- **Always await `Endpoint::close()`** before dropping an endpoint to prevent abrupt connection termination
- **Use `Endpoint::closed()`** to monitor when all background tasks and the UDP socket are fully released
- **Background tasks** terminate via cancellation token in `ShutdownState` once close is initiated
- **The UDP socket** persists until all endpoint clones are dropped after close completes
- **Individual connections** should be closed with `Connection::close` before endpoint shutdown for maximum clarity

## Frequently Asked Questions

### What happens if I drop an Endpoint without calling close?

Remote peers experience a connection timeout after approximately 3 seconds, appearing as a "connection lost" error in their application. Any data in flight may not be acknowledged, and the shutdown appears abnormal rather than intentional.

### How long does Endpoint::close() take to complete?

Typically less than 3 seconds. The method waits for QUIC close frames to be acknowledged by remote peers. On healthy networks this completes quickly; the timeout protects against unresponsive peers.

### Can I reopen an endpoint after closing it?

No. The `close` method consumes the endpoint's internal state and triggers cancellation of all background tasks. Create a new `Endpoint` with `Endpoint::builder()` if you need to resume networking.

### Do I need to close individual connections before closing the endpoint?

Not strictly required, but recommended. The endpoint's `close` method will drain all connections, but explicitly calling `Connection::close` first gives you control over error codes and reason strings sent to peers.