How to Properly Shut Down an Iroh Endpoint and Clean Up Resources
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 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, this async method forwards to EndpointInner::close and waits for the QUIC endpoint to finish draining.
// 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:
EndpointInnerowns aShutdownStatecontaining a cancellation token. Callingclosetriggers 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.
// 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:
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. 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_relaysiniroh/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, the socket is finally released only after:
close()has been called and completed- All clones of the
Endpointhave been dropped - The
Dropimplementation ofEndpointInnerexecutes
Complete Shutdown Example
Here is a production-ready pattern for shutting down an iroh endpoint:
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:
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. 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.
// 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
ShutdownStateonce close is initiated - The UDP socket persists until all endpoint clones are dropped after close completes
- Individual connections should be closed with
Connection::closebefore 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →