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

  1. Verify the local service answers on the configured port before enabling the funnel.
  2. Run `tailscale status` and `tailscale funnel status` to confirm the daemon is connected and the funnel is active.
  3. Re-authenticate tailscale (`tailscale login` / `tailscale up`) if the session expired.
  4. Confirm funnel is permitted for your tailnet (tailscale admin console / ACLs) and that MagicDNS is enabled.
  5. 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

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

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/d6e9cd0fc6d751d8. Report an issue: GitHub.