# Graceful Shutdown of Iroh Endpoints and Routers: A Complete Guide

> Learn how to gracefully shut down iroh endpoints and routers using endpoint.close().await for clean QUIC termination and router shutdown. Ensure UDP sockets close properly.

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

---

**To gracefully shut down an iroh endpoint, call `endpoint.close().await` to initiate QUIC connection termination and router shutdown, then optionally await `endpoint.closed()` to block until the underlying QUIC stack finishes, allowing the final drop to close UDP sockets.**

The iroh networking stack from n0-computer/iroh provides robust QUIC-based communication through the `Endpoint` struct, which manages UDP sockets, QUIC connections, and background routing tasks. When building production applications, implementing a **graceful shutdown of iroh endpoints and routers** prevents packet loss and ensures clean resource cleanup. This guide examines the actual implementation in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) and [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) to show you exactly how to terminate endpoints correctly.

## Understanding the Iroh Endpoint Architecture

### The Endpoint and Its Router

The `Endpoint` is a high-level handle that owns a QUIC endpoint instance, UDP sockets, and background actors including the router and relay managers. According to the source code in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs), the router is spawned as a background task during `EndpointInner::bind` and manages protocol frame multiplexing.

### Shutdown Targets

When shutting down, the system must accomplish four critical tasks:

- **Close the QUIC endpoint** by sending `ConnectionClose` frames with error code 0, draining pending streams, and waiting for retransmissions to complete.
- **Signal the router** to stop processing via an internal shutdown channel (`ControlMessage::Shutdown`).
- **Preserve UDP sockets** until all `Endpoint` clones are dropped to prevent panics in background tasks that may still reference the socket.
- **Detach watchers** automatically when the last `Endpoint` reference disappears, as their lifetimes are tied to the endpoint inner.

## Step-by-Step Graceful Shutdown Sequence

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

Call `Endpoint::close` (defined in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) at line 61) to begin the process. This method sends QUIC `ConnectionClose` frames to all peers, drains pending streams, and dispatches a shutdown command to the router task via the internal control channel.

### Step 2: Wait for QUIC Stack Completion

Use `Endpoint::closed` (line 170 of [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)) to obtain an `EndpointClosed` future. Awaiting this future blocks until the underlying QUIC implementation finishes retransmissions and signals cancellation, ensuring no packets are lost during termination.

### Step 3: Drop Final References

Once the last clone of the `Endpoint` is dropped, `EndpointInner::is_closed` (line 236 of [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs)) allows the UDP sockets to close. This reference-counted approach ensures background tasks never access invalid socket handles.

## Code Examples

### Basic Graceful Shutdown

This example demonstrates the minimal sequence for clean termination:

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

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build and bind a minimal endpoint.
    let ep = Endpoint::builder(presets::Minimal)
        .alpns(vec![b"demo-alpn".to_vec()])
        .bind()
        .await?;

    // … use the endpoint for connections …

    // Start a graceful shutdown.
    ep.close().await;               // send QUIC close and shutdown router
    ep.closed().await;              // wait for the QUIC stack to finish

    // All clones are dropped here, UDP sockets are finally closed.
    Ok(())
}

```

### Coordinating Background Tasks

Use `EndpointClosed::run_until` to spawn work that automatically stops when the endpoint shuts down:

```rust
use iroh::{Endpoint, endpoint::presets};
use n0_future::FutureExt; // for `.run_until`

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let ep = Endpoint::builder(presets::Minimal).bind().await?;
    let endpoint_closed = ep.closed();

    // Spawn a background task that does work until the endpoint closes.
    tokio::spawn(endpoint_closed.run_until(async {
        loop {
            // ... periodic work ...
            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        }
    }));

    // When you are done with the endpoint:
    ep.close().await;   // triggers the above task to stop
    Ok(())
}

```

*Key lines:* `EndpointClosed::run_until` (line 224 of [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs)).

### Advanced: Direct Router Control

For library authors who need explicit confirmation that the router task has exited, you can use the internal control channel directly:

```rust
use iroh::socket::router::ControlMessage;
use futures::channel::mpsc::UnboundedSender;

fn shutdown_router(ctrl_tx: UnboundedSender<ControlMessage>) {
    // Send a shutdown request; the router will exit after processing any
    // pending packets.
    let _ = ctrl_tx.unbounded_send(ControlMessage::Shutdown);
}

```

*Key lines:* Router receives `ControlMessage::Shutdown` in `socket::router` (see [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs)).

## Key Source Files and Implementation Details

| File | Purpose |
|------|---------|
| [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) | Public API for building, using, and shutting down an `Endpoint`. Contains `close()`, `closed()`, and the internal `EndpointClosed` future. |
| [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) | Sets up the low-level QUIC endpoint, creates the router task in `EndpointInner::bind`, and forwards the shutdown command via `EndpointInner::close`. |
| [`iroh/src/socket/router.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/router.rs) | Implements the router that demultiplexes protocol frames; reacts to `ControlMessage::Shutdown`. |

## Summary

- **Call `endpoint.close().await`** to initiate QUIC shutdown, notify the router, and start the graceful-close handshake.
- **Await `endpoint.closed().await`** to block until the QUIC stack reports that the close is complete and all connections are terminated.
- **Drop all `Endpoint` clones** to trigger final UDP socket cleanup via `is_closed` guards in the inner implementation.
- **Router lifecycle is automatic**—the background task receives shutdown signals through internal channels when `close()` is called, requiring no manual intervention.

## Frequently Asked Questions

### Do I need to manually stop the router?

No. The router does not expose a public API for shutdown; its lifecycle is tied to the `Endpoint`. When `EndpointInner::close` runs, it automatically sends a `ControlMessage::Shutdown` to the router's control channel, causing the task to exit after processing remaining packets.

### What happens if I drop the endpoint without calling close()?

Dropping the endpoint without calling `close()` will eventually clean up resources, but it may not send graceful QUIC `ConnectionClose` frames to peers. This can leave connections in a half-open state on the remote side. Always call `close().await` for production applications to ensure clean protocol termination.

### How do I ensure all connections are fully terminated before my program exits?

First, await `endpoint.close().await` to initiate the close handshake. Then, await `endpoint.closed().await` to block until the underlying QUIC endpoint signals that all connections have finished retransmissions and closed properly. This guarantees that no pending data is lost.

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

No, an `Endpoint` cannot be reused after `close()` has been called. You must create a new `Endpoint` instance using `Endpoint::builder()` and bind to new sockets. The original UDP sockets are permanently closed once the last reference is dropped.