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
- Identify and stop the occupying process: lsof -i :8787 (or the port in the message), then kill it
- Or configure a different port for Headroom via proxyPort / a different local proxyUrl
- 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
- Give Headroom a non-default dedicated port in shared/CI environments
- Run a port pre-check in deployment scripts before enabling autoStart
- Clean up zombie listeners from previous runs as part of the environment bootstrap
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
- Service reachable at ${explicitUrl}, but it does not appear
- Attempted to start Headroom proxy, but it was not reachable
- No usable Headroom launcher found. Tried PATH, local npm, gl
- Headroom proxy startup is disabled
- Remote Headroom proxy not reachable at ${explicitUrl}. Ensur
AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15).
Data as JSON: /api/errors/66e45dfc1857bb5e.
Report an issue: GitHub.