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
- Free the conflicting port: find the holder with `ss -ltnp | grep <port>` and stop it.
- Configure a different, unprivileged metrics_port for this service.
- Use port 0 in test setups to get an ephemeral port automatically.
- 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
- Give every linera process on a host a unique metrics port; document them per service.
- Use port 0 for ephemeral metrics ports in tests and read back the actual port from logs.
- Add a post-start check: curl http://localhost:<port>/metrics must return 200; alert if not, since the bind panic is swallowed by the spawned task.
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
- a running notification server
- owner should be different from spender
- invalid block export configuration: {message}
- Returned AccountInfo should have code: Some(...) and so code
- Returned AccountInfo should have code: Some(...) and so code
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/4c0a91fae5a8e330.
Report an issue: GitHub.