decolua/9router · error · Error
Health check timeout after ${HEALTH_CHECK.timeoutMs}ms
Error message
Health check timeout after ${HEALTH_CHECK.timeoutMs}ms What it means
waitForHealth() polls the tunnel URL every HEALTH_CHECK.intervalMs for up to HEALTH_CHECK.timeoutMs; if probeUrlAlive() never returns true within that window it throws 'Health check timeout after <N>ms'. It means the tunnel was enabled but the public URL never answered HTTP within the allotted time.
Source
Thrown at src/lib/tunnel/cloudflare/healthCheck.js:28
try {
const res = await fetch(`${url}/api/health`, {
signal: AbortSignal.timeout(HEALTH_CHECK.fetchTimeoutMs),
});
return res.ok;
} catch {
return false;
}
}
export async function waitForHealth(url, cancelToken = { cancelled: false }) {
const start = Date.now();
while (Date.now() - start < HEALTH_CHECK.timeoutMs) {
if (cancelToken.cancelled) throw new Error("cancelled");
if (await probeUrlAlive(url)) return true;
await new Promise((r) => setTimeout(r, HEALTH_CHECK.intervalMs));
}
throw new Error(`Health check timeout after ${HEALTH_CHECK.timeoutMs}ms`);
}
View on GitHub (pinned to 90b52e06ff)
Solutions
- Verify the local service is actually listening on the configured port (curl http://localhost:20128/dashboard).
- Check cloudflared logs for edge connection or authentication failures.
- Increase HEALTH_CHECK.timeoutMs / adjust intervalMs in src/lib/tunnel/cloudflare/healthCheck.js if the network is slow.
- Confirm outbound HTTPS to Cloudflare edge (region1/region2.v2.argotunnel.com, port 7844 or 443) is not blocked.
- Retry enableTunnel() once connectivity is restored.
Defensive patterns
Strategy: retry
Validate before calling
// Preflight: confirm local service is up before enabling the tunnel
const res = await fetch(`http://localhost:${port}/dashboard`).catch(() => null);
if (!res || !res.ok) throw new Error(`Local service not listening on ${port}`); Type guard
const isHealthTimeout = (e) => e instanceof Error && /Health check timeout after \d+ms/.test(e.message);
Try / catch
try {
await waitForHealth(url, token);
} catch (e) {
if (isHealthTimeout(e)) {
// retry once, or surface actionable message about local port / egress
return retryEnableOnce();
}
throw e;
} Prevention
- Always verify the local app responds on localPort before enabling a tunnel.
- Size HEALTH_CHECK.timeoutMs generously (e.g. 30-60s) for slow networks.
- Monitor cloudflared process logs; exit of the binary guarantees timeout.
- Check corporate firewall egress to Cloudflare edge before deploying.
When it happens
Trigger: enableTunnel() creates a cloudflared tunnel whose public URL stays unreachable for the whole timeout window: cloudflared failed to connect to Cloudflare edge, the local service on localPort isn't listening, network egress to Cloudflare is blocked, or the tunnel binary crashed after startup.
Common situations: Local app not started yet (port 20128 not listening), firewall/proxy blocking outbound connections to Cloudflare, cloudflared binary failing auth or quota limits, DNS propagation delay for a newly named tunnel, or an overly small HEALTH_CHECK.timeoutMs for slow networks.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Health check timeout after ${HEALTH_CHECK.timeoutMs}ms
- [Tunnel] direct URL not reachable yet, continuing via public
- cancelled
- [Tunnel] cloudflared exited unexpectedly, scheduling respawn
- [Tailscale] health check timed out, will retry via watchdog
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/b09d0664975c4e3b.
Report an issue: GitHub.