NanmiCoder/MediaCrawler · error · RuntimeError

Cannot find available port, tried {start_port} to {port-1}

Error message

Cannot find available port, tried {start_port} to {port-1}

What it means

Raised by BrowserLauncher._find_available_port() after trying to bind 100 consecutive TCP ports starting at start_port on localhost and finding all occupied. The browser's remote-debugging port must be bindable, so an exhausted port range aborts launch.

Source

Thrown at tools/browser_launcher.py:117

            if os.path.isfile(path) and os.access(path, os.X_OK):
                paths.append(path)

        return paths

    def find_available_port(self, start_port: int = 9222) -> int:
        """
        Find available port
        """
        port = start_port
        while port < start_port + 100:  # Try up to 100 ports
            try:
                with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                    s.bind(('localhost', port))
                    return port
            except OSError:
                port += 1

        raise RuntimeError(f"Cannot find available port, tried {start_port} to {port-1}")

    def launch_browser(self, browser_path: str, debug_port: int, headless: bool = False,
                      user_data_dir: Optional[str] = None) -> subprocess.Popen:
        """
        Launch browser process
        """
        # Basic launch arguments
        args = [
            browser_path,
            f"--remote-debugging-port={debug_port}",
            "--remote-debugging-address=0.0.0.0",  # Allow remote access
            "--no-first-run",
            "--no-default-browser-check",
            "--disable-background-timer-throttling",
            "--disable-backgrounding-occluded-windows",
            "--disable-renderer-backgrounding",
            "--disable-features=TranslateUI",
            "--disable-ipc-flooding-protection",

View on GitHub (pinned to d6f7c5bb90)

Solutions

  1. Free the stuck ports: kill leftover chrome/chromium processes (pkill -f remote-debugging-port) or the services holding them
  2. Give each worker a distinct start port far apart (e.g. base 9222 + worker_id*200) so ranges never overlap
  3. Check occupancy first: ss -ltnp | grep -E '922[0-9]' to see what holds the range
  4. Raise the retry count or make start_port configurable so a busy block can be skipped

Example fix

// before
port = start_port
while port < start_port + 100:
    ...

// after — per-worker disjoint range
start_port = 9222 + worker_id * 200  # ranges never collide
Defensive patterns

Strategy: fallback

Validate before calling

import socket

def port_free(port: int) -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        try:
            s.bind(("localhost", port))
            return True
        except OSError:
            return False

if not any(port_free(p) for p in range(start_port, start_port + 100)):
    raise SystemExit(f"port block {start_port}-{start_port+99} exhausted; free ports or change start_port")

Try / catch

try:
    port = launcher._find_available_port(start_port)
except RuntimeError:
    # fallback: widen the search range before giving up
    port = launcher._find_available_port(start_port + 1000)

Prevention

When it happens

Trigger: start_port is in a busy range — e.g. starting at 9222 while other Chrome instances, prior crashed crawler runs, or other services hold ports 9222-9321. Each iteration binds and releases, so only genuinely occupied (or permission-denied) ports advance the loop.

Common situations: Many parallel crawler workers each launching a browser with the same start port; zombie chrome processes from a previous run still holding debugging ports; Docker/CI environments where the ephemeral range or a same-range service occupies the block; ports below 1024 without privileges.

Related errors


AI-assisted analysis of NanmiCoder/MediaCrawler@d6f7c5bb90 (2026-08-15). Data as JSON: /api/errors/052cbdaaf8930ab5. Report an issue: GitHub.