abi/screenshot-to-code · critical · RuntimeError

No available port found from {start_port} to {start_port + m

Error message

No available port found from {start_port} to {start_port + max_attempts - 1}

What it means

RuntimeError("No available port found from {start} to {end}") raised by find_available_port() in backend/start.py when every port in the inclusive-at-start, exclusive-at-end range fails is_port_available()'s bind() test. start.py uses this to pick a uvicorn port (default 7001, 20 attempts => 7001-7020), so the failure means the backend cannot start at all. bind() fails on OSError, covering ports already bound, ports in exclusive use, and addresses/interfaces that cannot be bound on the chosen --host.

Source

Thrown at backend/start.py:23


def is_port_available(host: str, port: int) -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            sock.bind((host, port))
        except OSError:
            return False

    return True


def find_available_port(host: str, start_port: int, max_attempts: int) -> int:
    for port in range(start_port, start_port + max_attempts):
        if is_port_available(host, port):
            return port

    raise RuntimeError(
        f"No available port found from {start_port} to "
        f"{start_port + max_attempts - 1}"
    )


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--host", default="127.0.0.1")
    parser.add_argument("--port", type=int, default=7001)
    parser.add_argument("--max-port-attempts", type=int, default=20)
    args = parser.parse_args()

    port = find_available_port(args.host, args.port, args.max_port_attempts)
    if port != args.port:
        print(f"Port {args.port} is in use. Starting backend on port {port}.")

    uvicorn.run("main:app", host=args.host, port=port, reload=True)

View on GitHub (pinned to d026163f58)

Solutions

  1. Find and kill the squatters: ss -ltnp 'sport >= :7001' (or lsof -i :7001-7020), then kill the stale PIDs
  2. Start on a different base port: python start.py --port 7100
  3. Widen the scan: python start.py --max-port-attempts 100
  4. If --host was set to a specific interface, verify it is up (ip addr) or fall back to the default 127.0.0.1

Example fix

# before
python start.py  # RuntimeError: No available port found from 7001 to 7020

# after
python start.py --port 7100  # or first: kill $(lsof -t -i :7001-7020)
Defensive patterns

Strategy: fallback

Validate before calling

import socket

def ports_free(host: str, start: int, count: int) -> list[int]:
    free = []
    for port in range(start, start + count):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            try:
                s.bind((host, port)); free.append(port)
            except OSError:
                pass
    return free  # empty list => start.py will raise RuntimeError

Try / catch

try:
    port = find_available_port(host, start_port, max_attempts)
except RuntimeError:
    port = 7100  # fallback: fixed alternate port, or kill stale servers first (lsof -t -i :7001-7020 | xargs kill)

Prevention

When it happens

Trigger: Starting the backend with defaults while 20 prior dev-server instances (or other apps) hold 7001-7020; passing --host of an interface that is down or not owned by the process; running under an environment where loopback binding is restricted.

Common situations: Repeated `uvicorn main:app --reload` sessions that never got killed; Docker port mappings consuming the range; another service (e.g. airtower/ADS-B on 7001-adjacent ports or a previously orphaned start.py) squatting the range.


AI-assisted analysis of abi/screenshot-to-code@d026163f58 (2026-08-14). Data as JSON: /api/errors/8117fcfee01ddb82. Report an issue: GitHub.