headroomlabs-ai/headroom · error · Error

Cannot auto-start Headroom at ${startupUrl}: port is in use

Error message

Cannot auto-start Headroom at ${startupUrl}: port is in use by a non-Headroom service (${startupProbe.reason ?? "unknown service"}).

What it means

Auto-start is enabled, and the port the manager wants to launch Headroom on (explicit local proxyUrl or the default candidate, default port 8787) is already occupied by a service that is not Headroom. Rather than clobbering an unrelated process or launching a Headroom instance that cannot bind, the manager aborts with the occupying service's identity in the reason string.

Source

Thrown at plugins/openclaw/src/proxy-manager.ts:118

        throw new Error(
          `Service reachable at ${explicitUrl}, but it does not appear to be a Headroom proxy (${explicitProbe.reason ?? "unknown service"}).`,
        );
      }
    }

    // Remote URLs are connect-only — never auto-start a subprocess for them
    if (explicitUrl && !isLocalProxyUrl(explicitUrl)) {
      throw new Error(
        `Remote Headroom proxy not reachable at ${explicitUrl}. Ensure the proxy is running at that address.`,
      );
    }

    // Auto-start is only available for local proxies
    if (this.config.autoStart === true) {
      const startupUrl = explicitUrl ?? defaultCandidates[0];
      const startupProbe = probeByUrl.get(startupUrl);
      if (startupProbe?.reachable && !startupProbe.isHeadroom) {
        throw new Error(
          `Cannot auto-start Headroom at ${startupUrl}: port is in use by a non-Headroom service (${startupProbe.reason ?? "unknown service"}).`,
        );
      }

      this.logger.info(
        `No Headroom proxy detected${explicitUrl ? ` at ${startupUrl}` : " on default local endpoints"}; attempting to auto-start...`,
      );
      await this.startHeadroomProxy(startupUrl, port);

      const startedProbe = await waitForHeadroomProxy(
        startupUrl,
        this.config.startupTimeoutMs ?? 20_000,
      );
      if (startedProbe.reachable && startedProbe.isHeadroom) {
        this.proxyUrl = startupUrl;
        this.logger.info(`Headroom proxy started and reachable at ${startupUrl}`);
        return startupUrl;
      }

View on GitHub (pinned to 322425c43b)

Solutions

  1. Identify and stop the occupying process: lsof -i :8787 (or the port in the message), then kill it
  2. Or configure a different port for Headroom via proxyPort / a different local proxyUrl
  3. If the occupying service is actually a stale Headroom instance that fails the probe, kill it so a clean one can start

Example fix

// before: autoStart on default port 8787 occupied by a dev server
manager.configure({ autoStart: true });

// after: move Headroom to a free port
manager.configure({ autoStart: true, proxyPort: 8790 });
Defensive patterns

Strategy: validation

Validate before calling

import { probeHeadroomProxy } from "./proxy-probe.js";

async function portFreeForHeadroom(url: string): Promise<boolean> {
  const probe = await probeHeadroomProxy(url);
  // Safe to auto-start only when nothing answers, or Headroom itself is there
  return !probe.reachable || probe.isHeadroom;
}

if (!await portFreeForHeadroom(startupUrl)) {
  throw new Error(`Port occupied by a non-Headroom service — free it or change proxyPort`);
}

Type guard

interface Probe { reachable: boolean; isHeadroom: boolean }

function canAutoStart(probe: Probe | undefined): boolean {
  return !probe || !probe.reachable || probe.isHeadroom;
}

Try / catch

try {
  await manager.resolveProxyUrl();
} catch (e) {
  if (e instanceof Error && e.message.includes("port is in use by a non-Headroom service")) {
    // deterministic conflict: configure a different proxyPort instead of retrying
    manager.configure({ proxyPort: nextFreePort() });
    await manager.resolveProxyUrl();
  } else throw e;
}

Prevention

When it happens

Trigger: config.autoStart === true, and the startup URL's port responds to the probe as reachable-but-not-Headroom. Typical when a previous non-Headroom dev server, another proxy, or an orphaned process holds the default port.

Common situations: Long-lived dev server sharing the 8787 default; a crashed earlier run left a zombie listener; two tools configured with the same default port; another developer's service on a shared machine.

Related errors


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