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
The tailscale waitForHealth() polls the funnel URL until probeUrlAlive() succeeds, bounded by HEALTH_CHECK.timeoutMs; if the funnel never becomes reachable in time it throws 'Health check timeout after <N>ms'. It indicates the tailscale funnel was started but its public URL never served HTTP within the window.
Source
Thrown at src/lib/tunnel/tailscale/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 answers on the configured port before enabling the funnel.
- Run `tailscale status` and `tailscale funnel status` to confirm the daemon is connected and the funnel is active.
- Re-authenticate tailscale (`tailscale login` / `tailscale up`) if the session expired.
- Confirm funnel is permitted for your tailnet (tailscale admin console / ACLs) and that MagicDNS is enabled.
- Increase HEALTH_CHECK.timeoutMs in src/lib/tunnel/tailscale/healthCheck.js on slow networks, then retry.
Defensive patterns
Strategy: retry
Validate before calling
// Preflight: is tailscale ready and is the local app up?
execSync("tailscale status", { stdio: "ignore" }); // throws if not connected
const res = await fetch(`http://localhost:${port}`).catch(() => null);
if (!res?.ok) throw new Error("local app not ready"); Type guard
const isHealthTimeout = (e) => e instanceof Error && /Health check timeout after \d+ms/.test(e.message);
Try / catch
try {
await waitForHealth(funnelUrl, token);
} catch (e) {
if (isHealthTimeout(e)) {
console.error("Funnel never became healthy — check `tailscale status`, auth, and ACLs");
return retryAfterCheck();
}
throw e;
} Prevention
- Run `tailscale up`/login and `tailscale status` before enabling funnel.
- Confirm funnel is allowed for your tailnet and MagicDNS is enabled.
- Ensure the local service listens on the port passed to enableTailscale.
- Increase timeout on high-latency networks and retry once before surfacing the error.
When it happens
Trigger: enableTailscale() completes `tailscale funnel` spawn but the URL stays dead for the full timeout: tailscale not logged in / expired auth, funnel disabled on the tailnet (ACL/policy), tailnet DNS not propagated, the local port not listening, or tailscaled disconnected from the DERP/coordination server.
Common situations: Fresh tailscale install where `tailscale up` was never run, corporate networks blocking tailscale UDP, funnel feature not enabled for the tailnet, slow MagicDNS propagation for a new hostname, or the local app (port 20128) not yet started.
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
- [Tailscale] health check timed out, will retry via watchdog
- cancelled
- [Tunnel] direct URL not reachable yet, continuing via public
- cancelled
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/d6e9cd0fc6d751d8.
Report an issue: GitHub.