ZhuLinsen/daily_stock_analysis · critical · RuntimeError

FastAPI port is not available: {host}:{port}

Error message

FastAPI port is not available: {host}:{port}

What it means

Before handing the port to uvicorn, main.py probes it by binding a raw socket; if the OS refuses the bind, startup aborts with RuntimeError. Bind failures mean either another process already listens on host:port (EADDRINUSE) or the host address does not exist on this machine (e.g. binding a LAN IP while offline, or IPv6 syntax on an IPv4-only stack).

Source

Thrown at main.py:1199

def start_api_server(host: str, port: int, config: Config) -> None:
    """
    在后台线程启动 FastAPI 服务

    Args:
        host: 监听地址
        port: 监听端口
        config: 配置对象
    """
    import socket
    import threading
    import uvicorn

    probe = socket.socket(socket.AF_INET6 if ":" in host else socket.AF_INET, socket.SOCK_STREAM)
    try:
        probe.bind((host, port))
    except OSError as exc:
        raise RuntimeError(f"FastAPI port is not available: {host}:{port}") from exc
    finally:
        probe.close()

    level_name = (config.log_level or "INFO").lower()
    use_config_signal_handlers = True
    uvicorn_kwargs = {
        "host": host,
        "port": port,
        "log_level": level_name,
        "log_config": None,
    }
    # Import the ASGI app object in the calling thread instead of handing uvicorn
    # the "api.app:app" import string. With the string, uvicorn imports the app
    # lazily inside the server thread, and that import (litellm + the full app
    # tree, ~10s+ on constrained hosts) runs inside the startup probe window
    # below, tripping the 3.0s timeout and causing a restart loop on slower
    # machines. Importing first keeps the heavy work out of the probe window;
    # genuine import failures still surface immediately to the caller.

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Find and stop the occupant: ss -ltnp 'sport = :<port>' or lsof -i :<port>, then kill it — or start your instance on a different port.
  2. Verify the host value resolves on this machine: binding 0.0.0.0 or 127.0.0.1 always works; a specific LAN IP fails when the interface is absent.
  3. If a stale process from a crashed run holds the socket, kill it (ps aux | grep main.py) rather than changing ports.
  4. In Docker, check for duplicate port mappings in docker-compose.

Example fix

# before
$ python main.py --serve  # RuntimeError: FastAPI port is not available: 0.0.0.0:8000

# after
$ ss -ltnp 'sport = :8000'   # find PID holding 8000
$ kill <PID>
$ python main.py --serve
Defensive patterns

Strategy: validation

Validate before calling

import socket

def port_free(host: str, port: int) -> bool:
    family = socket.AF_INET6 if ":" in host else socket.AF_INET
    with socket.socket(family, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            s.bind((host, port))
            return True
        except OSError:
            return False

assert port_free(host, port), f"{host}:{port} occupied"

Try / catch

try:
    run_fastapi(host, port, config)
except RuntimeError as e:
    if "port is not available" in str(e):
        port = find_free_port()  # or prompt user; then retry
        run_fastapi(host, port, config)
    else:
        raise

Prevention

When it happens

Trigger: Previous instance of the server (--serve/--serve-only) still running; another service occupying the port (default uvicorn port 8000 is commonly taken); host set to an interface that is down or misconfigured; Docker container binding a port already published by a sibling container; IPv6 host string on a kernel without IPv6.

Common situations: Running python main.py --serve twice; port 8000 grabbed by another dev server; config/env PORT mismatch after copying .env between machines; firewall/SELinux denying bind on non-loopback hosts.

Related errors


AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15). Data as JSON: /api/errors/6db03d29b8e2fda1. Report an issue: GitHub.