How to Gracefully Shut Down an iroh Endpoint and Router

Call endpoint.close().await to initiate QUIC shutdown and notify the internal router, then optionally await endpoint.closed().await to block until the underlying QUIC stack finishes, and finally drop the Endpoint to release UDP sockets.

The iroh networking stack (n0-computer/iroh) centers on the Endpoint, a high-level object that manages a QUIC endpoint, UDP sockets, and background actors including the router and relay managers. When you are done with an endpoint, you must shut it down cleanly to prevent data loss and ensure all connections close properly. This guide explains how to gracefully shut down an iroh Endpoint and its associated Router using the public API and internal mechanics found in the source code.

Understanding the iroh Endpoint Architecture

The Endpoint and Its Internal Router

The Endpoint in iroh/src/endpoint.rs is not merely a wrapper around a QUIC connection. According to the iroh source code, it owns a QUIC endpoint, UDP sockets, and a set of background actors. The router, created in socket::EndpointInner::bind (see iroh/src/socket.rs line 84), is a background task that multiplexes protocol frames. When you initiate a shutdown, the router must stop processing new packets and exit cleanly.

Connection Lifecycle and Resource Management

When you call Endpoint::close (line 61 of iroh/src/endpoint.rs), the implementation in EndpointInner::close sends a QUIC ConnectionClose with error-code 0, drains pending streams, and waits for the QUIC stack to finish retransmissions. The router receives a shutdown command via an internal channel when EndpointInner::close runs, ensuring no packet is lost during shutdown.

Graceful Shutdown Steps

Step 1: Initiate Shutdown with close()

Begin by calling endpoint.close().await. This method, defined in iroh/src/endpoint.rs, triggers EndpointInner::close, which sends a QUIC close frame and notifies the router via a Shutdown message on the router's control channel (socket::router::ControlMessage::Shutdown). This starts the graceful-close handshake.

Step 2: Wait for Completion with closed()

To block until the QUIC stack reports that the close is complete, await endpoint.closed(). This returns an EndpointClosed future (line 170 of iroh/src/endpoint.rs) that resolves only after the underlying QUIC endpoint signals cancellation. This ensures all connections are fully terminated before proceeding.

Step 3: Drop the Endpoint to Clean Up Sockets

Once the close is complete, drop the Endpoint or let it go out of scope. The UDP sockets stay alive while any clone of Endpoint exists, guarded by EndpointInner::is_closed (line 236 of iroh/src/socket.rs). When the last clone is dropped, the sockets are finally closed, and watchers (such as address-watchers created from EndpointInner) are automatically detached (line 124 of iroh/src/endpoint.rs).

Code Examples

Basic Graceful Shutdown Pattern

The following example demonstrates the standard shutdown sequence using the public API:

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 with Shutdown

If you spawn tasks that should stop when the endpoint shuts down, use the EndpointClosed future with run_until (line 224 of iroh/src/endpoint.rs):

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(())
}

Advanced: Explicitly Waiting for the Router

For library authors who need to ensure the router task has exited, you can use the internal control channel directly (advanced usage):

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 Source Files and Implementation Details

The graceful shutdown behavior is implemented across several files in the n0-computer/iroh repository:

  • iroh/src/endpoint.rs: Contains the public API for the Endpoint, including close() at line 61, closed() at line 170, and the EndpointClosed future with run_until() at line 224.
  • iroh/src/socket.rs: Sets up the low-level QUIC endpoint and creates the router task at line 84. Also contains EndpointInner::is_closed at line 236 to guard UDP socket closure.
  • iroh/src/socket/mod.rs: Re-exported as socket, holds the EndpointInner implementation including the close logic and socket management.
  • 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 and notify the router via an internal channel.
  • Await endpoint.closed().await to block until the QUIC stack finishes all retransmissions and connection cleanup.
  • Drop the Endpoint (or let it go out of scope) to close UDP sockets once the last clone is dropped.
  • Watchers and background tasks are automatically cleaned up when the last Endpoint clone is dropped, with no extra API calls required.
  • The router lifecycle is managed internally; it receives the shutdown command automatically when you call close().

Frequently Asked Questions

What happens if I don't call close() before dropping the Endpoint?

If you drop the Endpoint without calling close(), the QUIC endpoint will still close, but you may not wait for pending data to be transmitted or for connections to close gracefully. The UDP sockets will remain open until the last Endpoint clone is dropped due to EndpointInner::is_closed guards in iroh/src/socket.rs, but active connections might receive an abrupt termination rather than a graceful ConnectionClose frame.

How do I know when all connections are fully terminated?

Await the EndpointClosed future returned by endpoint.closed() (line 170 of iroh/src/endpoint.rs). This future resolves only after the underlying QUIC endpoint signals that it has finished retransmissions and all connections are closed. This ensures no data is lost during shutdown.

Can I force an immediate shutdown without waiting for connections?

While the public API emphasizes graceful shutdown via close(), dropping the Endpoint immediately will eventually close the sockets. However, this is not recommended for production code as it bypasses the graceful QUIC handshake. The source code in iroh/src/endpoint.rs shows that close() sends a proper ConnectionClose frame with error-code 0, allowing peers to detect a clean shutdown rather than a timeout.

Is the router shutdown automatic or do I need to stop it manually?

The router shutdown is automatic. When you call endpoint.close().await, the implementation in EndpointInner::close sends a ControlMessage::Shutdown to the router's control channel. The router task, created in socket::EndpointInner::bind (line 84 of iroh/src/socket.rs), processes this message and exits after handling any remaining inbound packets. No manual intervention is required.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →