{"record":{"id":"d3ce7333e79e68b6","repo":"block/buzz","slug":"failed-to-bind-health-port-e","errorCode":null,"errorMessage":"Failed to bind health port {}: {e}","messagePattern":"Failed to bind health port (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/buzz-relay/src/main.rs","lineNumber":1257,"sourceCode":"/// `state.rs`) sum to 25s and stay inside the 30s hard drain. Total worst\n/// case from SIGTERM to forced exit is 5s + 30s = 35s. Both fit inside the\n/// chart's `terminationGracePeriodSeconds: 60` (`deploy/charts/buzz/values.yaml`),\n/// which leaves headroom but assumes no `preStop` hook adds further delay.\n/// With jitter off (`BUZZ_DRAIN_JITTER_MS=0`, the default) sockets close\n/// all-at-once right after the grace, so the per-socket delay collapses to\n/// roughly the 5s grace plus the ack wait.\nconst GRACEFUL_DRAIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);\n\nasync fn serve(\n    router: axum::Router,\n    health_router: axum::Router,\n    state: Arc<AppState>,\n) -> anyhow::Result<()> {\n    let config = &state.config;\n\n    let health_listener = tokio::net::TcpListener::bind((\"0.0.0.0\", config.health_port))\n        .await\n        .map_err(|e| anyhow::anyhow!(\"Failed to bind health port {}: {e}\", config.health_port))?;\n    info!(port = config.health_port, \"Health probe listener started\");\n    tokio::spawn(async move {\n        axum::serve(health_listener, health_router).await.ok();\n    });\n\n    let (shutdown_tx, _) = tokio::sync::watch::channel(false);\n    let shutdown_flag = Arc::clone(&state.shutting_down);\n    let drain_conn_manager = Arc::clone(&state.conn_manager);\n    let drain_jitter_ms = state.config.drain_jitter_ms;\n    let tx = shutdown_tx.clone();\n    // TODO(coverage): `serve`'s shutdown wiring has no automated test. The\n    // jittered drain helper (`ConnectionManager::drain_all_jittered`) is\n    // covered in `state.rs`, but coverage of the helper is not coverage of\n    // its use here: the three wiring facts below are currently unguarded, and\n    // mutating any one of them leaves the suite green.\n    //   1. Jitter dispatch: `drain_jitter_ms == 0` must pick `drain_all`, and\n    //      a non-zero value must pick `drain_all_jittered(drain_jitter_ms)`.\n    //      A mutant that inverts this condition ships jitter-off in prod.","sourceCodeStart":1239,"sourceCodeEnd":1275,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/main.rs#L1239-L1275","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before: Error: Failed to bind health port 8080: Address already in use (os error 98)\nBUZZ_HEALTH_PORT=8080\n\n# after\nBUZZ_HEALTH_PORT=18080","handlingStrategy":"validation","validationCode":"// Pre-flight: confirm the health port is free before starting the relay.\nfn health_port_available(port: u16) -> bool {\n    std::net::TcpListener::bind((\"0.0.0.0\", port)).is_ok()\n}\n\nlet port: u16 = std::env::var(\"BUZZ_HEALTH_PORT\")\n    .ok()\n    .and_then(|v| v.parse().ok())\n    .unwrap_or(8080);\nassert!(health_port_available(port), \"health port {port} already in use\");","typeGuard":"fn is_addr_in_use(e: &std::io::Error) -> bool {\n    e.kind() == std::io::ErrorKind::AddrInUse\n}","tryCatchPattern":"let health_listener = match tokio::net::TcpListener::bind((\"0.0.0.0\", port)).await {\n    Ok(l) => l,\n    Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {\n        return Err(anyhow!(\"health port {port} busy — set BUZZ_HEALTH_PORT to a free port\"))\n    }\n    Err(e) => return Err(anyhow!(\"health port {port} bind failed: {e}\")),\n};","preventionTips":["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"],"tags":["rust","network","port-bind","health-check","startup","tokio"],"backgroundTag":"address-already-in-use","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}