headroomlabs-ai/headroom · error · RuntimeError

No available port found in range {start_port}-{end_port - 1}

Error message

No available port found in range {start_port}-{end_port - 1}

What it means

When starting the wrap proxy, Headroom probes a range of ports (start_port up to start_port + max_attempts, capped at 65536) by attempting to bind each one. Ports failing with EADDRINUSE (already bound) or EACCES (privileged/reserved on Linux/Windows) are skipped as unusable; any other OSError (e.g. EADDRNOTAVAIL) propagates immediately. If every port in the range is busy or privileged, this RuntimeError is raised.

Source

Thrown at headroom/cli/wrap.py:601

    return None


def _find_available_port(start_port: int, max_attempts: int = 100) -> int:
    """Find first available port >= start_port via socket.bind probe.

    Skips ports with EADDRINUSE (busy) and EACCES (reserved on Windows,
    privileged on Linux) — both indicate the port can't be bound here.
    Other OS errors (EADDRNOTAVAIL) propagate immediately.
    Raises RuntimeError when no port is found in range.
    """
    end_port = min(start_port + max_attempts, 65536)
    for port in range(start_port, end_port):
        error = _port_bind_error(port)
        if error is None:
            return port
        if error.errno not in (errno.EADDRINUSE, errno.EACCES):
            raise error
    raise RuntimeError(f"No available port found in range {start_port}-{end_port - 1}")


def _get_log_path() -> Path:
    """Get path for proxy log file."""
    from headroom import paths as _paths

    log_dir = _paths.log_dir()
    log_dir.mkdir(parents=True, exist_ok=True)
    return log_dir / "proxy.log"


def _get_proxy_stdio_log_path() -> Path:
    """Get path for dedicated proxy stdio capture."""
    return _get_log_path().with_name("proxy-stdio.log")


def _start_proxy(
    port: int,

View on GitHub (pinned to 322425c43b)

Solutions

  1. Free the occupied ports: find and kill stale proxies, e.g. lsof -i :4000-4099 then kill <pid>, or pkill -f headroom
  2. Pass an explicit port in an empty range, e.g. headroom wrap claude --port 8787
  3. Raise the attempt count / use a different start port so the scan covers a wider or freer range
  4. If EACCES on every port, rerun from an unprivileged high port range (>1024) or grant the process permission to bind

Example fix

# before
headroom wrap claude            # scans default range, all busy -> RuntimeError

# after
pkill -f 'headroom.*proxy'      # clear stale proxies
headroom wrap claude --port 8787
Defensive patterns

Strategy: validation

Validate before calling

import socket

def find_free_port(start: int, attempts: int = 100) -> int | None:
    for port in range(start, min(start + attempts, 65536)):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            try:
                s.bind(("127.0.0.1", port))
            except OSError:
                continue
            return port
    return None

assert find_free_port(4000) is not None, "No free port in wrap range; kill stale proxies first"

Try / catch

try:
    start_proxy()
except RuntimeError as e:
    if "No available port found in range" in str(e):
        # retry with an explicit, verified-free port
        port = find_free_port(20000)
        start_proxy(port=port)
    else:
        raise

Prevention

When it happens

Trigger: Running `headroom wrap <tool>` (or any code path that calls the port-finding helper) on a machine where all ports in the scanned range — typically starting near the requested port, e.g. 4000-4099 — are already bound by other processes, or where the process lacks permission to bind any of them (EACCES on all candidates).

Common situations: Many stale Headroom proxy processes left running from previous sessions exhaust the default range; dev machines crowded with other local servers (Vite, Docker, databases) occupying the scanned ports; running as a low-privilege user where the chosen range intersects reserved ports; a low ulimit or socket exhaustion causing binds to fail.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/47b673da503e5424. Report an issue: GitHub.