linera-io/linera-protocol · error

Failed to bind to address

Error message

Failed to bind to address

What it means

start_metrics (linera-metrics/src/monitoring_server.rs:91) binds a TcpListener for the /metrics HTTP server inside a tokio::spawn and expects bind to succeed. Failure means the address could not be bound: port already in use, permission denied on privileged ports (<1024) for non-root users, or an invalid/unavailable address. Because the bind happens in a detached spawned task, the panic does not kill the process - the service silently runs on without metrics, which makes this easy to miss.

Source

Thrown at linera-metrics/src/monitoring_server.rs:104

///
/// `register_metrics` is the caller's `init_metrics`. It is a parameter rather than a direct
/// call because this crate sits below `linera-views` and friends in the dependency graph and
/// cannot reach their metrics; taking it here makes forgetting to register a compile error
/// instead of a metric that silently only appears once its code path first runs.
pub fn start_metrics(
    address: impl ToSocketAddrs + Debug + Send + 'static,
    shutdown_signal: CancellationToken,
    memory_profiling: MemoryProfiling,
    register_metrics: impl FnOnce(),
) {
    crate::runtime_metrics::register();
    register_metrics();
    let app = metrics_router(memory_profiling);

    tokio::spawn(async move {
        let listener = tokio::net::TcpListener::bind(address)
            .await
            .expect("Failed to bind to address");
        let address = listener.local_addr().expect("Failed to get local address");

        info!("Starting to serve metrics on {:?}", address);
        if let Err(e) = axum::serve(listener, app)
            .with_graceful_shutdown(shutdown_signal.cancelled_owned())
            .await
        {
            panic!("Error serving metrics: {e}");
        }
    });
}

fn metrics_router(memory_profiling: MemoryProfiling) -> Router {
    #[cfg(feature = "jemalloc")]
    if memory_profiling == MemoryProfiling::Enabled {
        match MemoryProfiler::check_prof_ctl() {
            Ok(()) => {
                info!("Memory profiling enabled, registering /debug/pprof and /debug/flamegraph endpoints");

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Free the conflicting port: find the holder with `ss -ltnp | grep <port>` and stop it.
  2. Configure a different, unprivileged metrics_port for this service.
  3. Use port 0 in test setups to get an ephemeral port automatically.
  4. After startup, curl the /metrics endpoint to verify the server actually came up.
Defensive patterns

Strategy: validation

Validate before calling

// Verify the metrics port is free before starting the service:
async fn ensure_metrics_port_free(addr: std::net::SocketAddr) -> anyhow::Result<()> {
    match tokio::net::TcpListener::bind(addr).await {
        Ok(l) => { drop(l); Ok(()) }
        Err(e) => Err(anyhow::anyhow!("metrics address {addr} unavailable: {e}")),
    }
}

Prevention

When it happens

Trigger: Starting a linera service (validator, proxy, exporter via start_metrics_with_profiling) when metrics_port is already bound by another process, or configuring a privileged port while running non-root.

Common situations: Two linera components on one host configured with the same metrics port; port reused across container restarts with a stale holder; firewall/SELinux denying bind; port < 1024 in a non-root container.

Related errors


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/4c0a91fae5a8e330. Report an issue: GitHub.