block/buzz · error

Failed to bind health port {}: {e}

Error message

Failed to bind health port {}: {e}

What it means

In serve(), buzz-relay binds a dedicated health-probe listener on 0.0.0.0:BUZZ_HEALTH_PORT (default 8080, parsed at config.rs:716) before the main listener; this error wraps the tokio TcpListener::bind io::Error when the OS refuses that bind, and it is fatal at startup. The bind is hardcoded to 0.0.0.0 regardless of BUZZ_BIND_ADDR. Typical causes are AddrInUse (another process owns the port) and PermissionDenied (port < 1024 without CAP_NET_BIND_SERVICE).

Source

Thrown at crates/buzz-relay/src/main.rs:1257

/// `state.rs`) sum to 25s and stay inside the 30s hard drain. Total worst
/// case from SIGTERM to forced exit is 5s + 30s = 35s. Both fit inside the
/// chart's `terminationGracePeriodSeconds: 60` (`deploy/charts/buzz/values.yaml`),
/// which leaves headroom but assumes no `preStop` hook adds further delay.
/// With jitter off (`BUZZ_DRAIN_JITTER_MS=0`, the default) sockets close
/// all-at-once right after the grace, so the per-socket delay collapses to
/// roughly the 5s grace plus the ack wait.
const GRACEFUL_DRAIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);

async fn serve(
    router: axum::Router,
    health_router: axum::Router,
    state: Arc<AppState>,
) -> anyhow::Result<()> {
    let config = &state.config;

    let health_listener = tokio::net::TcpListener::bind(("0.0.0.0", config.health_port))
        .await
        .map_err(|e| anyhow::anyhow!("Failed to bind health port {}: {e}", config.health_port))?;
    info!(port = config.health_port, "Health probe listener started");
    tokio::spawn(async move {
        axum::serve(health_listener, health_router).await.ok();
    });

    let (shutdown_tx, _) = tokio::sync::watch::channel(false);
    let shutdown_flag = Arc::clone(&state.shutting_down);
    let drain_conn_manager = Arc::clone(&state.conn_manager);
    let drain_jitter_ms = state.config.drain_jitter_ms;
    let tx = shutdown_tx.clone();
    // TODO(coverage): `serve`'s shutdown wiring has no automated test. The
    // jittered drain helper (`ConnectionManager::drain_all_jittered`) is
    // covered in `state.rs`, but coverage of the helper is not coverage of
    // its use here: the three wiring facts below are currently unguarded, and
    // mutating any one of them leaves the suite green.
    //   1. Jitter dispatch: `drain_jitter_ms == 0` must pick `drain_all`, and
    //      a non-zero value must pick `drain_all_jittered(drain_jitter_ms)`.
    //      A mutant that inverts this condition ships jitter-off in prod.

View on GitHub (pinned to f956e6fe06)

Solutions

  1. Find the holder: ss -ltnp 'sport = :8080' (or lsof -i :8080), then stop it or pick another port.
  2. Set BUZZ_HEALTH_PORT to a free, correctly-formatted port (remember invalid values silently become 8080).
  3. For privileged ports, grant CAP_NET_BIND_SERVICE (container securityContext or setcap) or simply use a port > 1024.
  4. If running multiple relays on one host, give each its own BUZZ_HEALTH_PORT and BUZZ_BIND_ADDR.

Example fix

# before: Error: Failed to bind health port 8080: Address already in use (os error 98)
BUZZ_HEALTH_PORT=8080

# after
BUZZ_HEALTH_PORT=18080
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight: confirm the health port is free before starting the relay.
fn health_port_available(port: u16) -> bool {
    std::net::TcpListener::bind(("0.0.0.0", port)).is_ok()
}

let port: u16 = std::env::var("BUZZ_HEALTH_PORT")
    .ok()
    .and_then(|v| v.parse().ok())
    .unwrap_or(8080);
assert!(health_port_available(port), "health port {port} already in use");

Type guard

fn is_addr_in_use(e: &std::io::Error) -> bool {
    e.kind() == std::io::ErrorKind::AddrInUse
}

Try / catch

let health_listener = match tokio::net::TcpListener::bind(("0.0.0.0", port)).await {
    Ok(l) => l,
    Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {
        return Err(anyhow!("health port {port} busy — set BUZZ_HEALTH_PORT to a free port"))
    }
    Err(e) => return Err(anyhow!("health port {port} bind failed: {e}")),
};

Prevention

When it happens

Trigger: Booting with BUZZ_HEALTH_PORT unset (lands on 8080) while anything else listens on 8080; two relay instances sharing a host with the same env; BUZZ_HEALTH_PORT set to a privileged port in a container lacking NET_BIND_SERVICE. Note: an unparseable BUZZ_HEALTH_PORT silently falls back to 8080 (config uses and_then(|v| v.parse().ok())), so a typo can unexpectedly put you on a colliding port.

Common situations: 8080 occupied by proxies, admin UIs, or sidecars in the same pod; docker-compose replicas with host networking; dev machines already running another buzz service or common dev tooling on 8080.

Related errors


AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16). Data as JSON: /api/errors/d3ce7333e79e68b6. Report an issue: GitHub.