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
- Find the holder: ss -ltnp 'sport = :8080' (or lsof -i :8080), then stop it or pick another port.
- Set BUZZ_HEALTH_PORT to a free, correctly-formatted port (remember invalid values silently become 8080).
- For privileged ports, grant CAP_NET_BIND_SERVICE (container securityContext or setcap) or simply use a port > 1024.
- 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
- Assign each service a unique, documented health port in env templates (.env.example)
- Add a port pre-flight to container entrypoints before exec'ing the relay
- Remember an unparseable BUZZ_HEALTH_PORT silently becomes 8080 — validate the value yourself
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
- Failed to bind {}: {e}
- mesh endpoint bind on {} failed: {e}
- git conformance probe failed: {e}
- BUZZ_UDS_PATH {uds_path} exists but is not a socket
- Failed to bind UDS {uds_path}: {e}
AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16).
Data as JSON: /api/errors/d3ce7333e79e68b6.
Report an issue: GitHub.