decolua/9router · info

[Tailscale] health check timed out, will retry via watchdog

Error message

[Tailscale] health check timed out, will retry via watchdog

What it means

enableTailscale performs a best-effort health check of the funnel URL after setup. If waitForHealth throws a 'Health check timeout' error, it is downgraded to this warning and enablement still returns success (reachable=false), relying on the funnel watchdog to retry/verify later. Any other health-check error is re-thrown as a real enable failure.

Source

Thrown at src/lib/tunnel/tailscale/manager.js:95

      stopFunnel();
      return { success: false, error: "Tailscale not connected. Device may have been removed. Please re-login." };
    }

    await updateSettings({ tailscaleEnabled: true, tailscaleUrl: result.tunnelUrl });
    console.log(`[Tailscale] funnel up: ${result.tunnelUrl}`);

    // Provision TLS cert so Funnel can serve HTTPS (non-fatal if fails)
    const hostname = new URL(result.tunnelUrl).hostname;
    await provisionCert(hostname);

    // Verify funnel serves /api/health — timeout is non-fatal (DNS may still be propagating)
    let reachableNow = false;
    try {
      await waitForHealth(result.tunnelUrl, token);
      reachableNow = true;
    } catch (he) {
      if (!he.message.startsWith("Health check timeout")) throw he;
      console.warn(`[Tailscale] health check timed out, will retry via watchdog`);
    }
    console.log(`[Tailscale] enable success (reachable=${reachableNow})`);
    return { success: true, tunnelUrl: result.tunnelUrl };
  } catch (e) {
    console.error(`[Tailscale] enable error: ${e.message}`);
    throw e;
  } finally {
    svc.spawnInProgress = false;
  }
}

export async function disableTailscale() {
  console.log("[Tailscale] disable");
  svc.cancelToken.cancelled = true;
  stopFunnel();
  await updateSettings({ tailscaleEnabled: false, tailscaleUrl: "" });
  return { success: true };
}

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Wait — the watchdog retries the health check automatically; verify reachability shortly after.
  2. Verify 'tailscale funnel status' / 'tailscale status' shows the funnel active and the hostname correct.
  3. Check that tailscaled is running and the device is connected to the tailnet.
  4. If permanently unreachable, disable/re-enable the funnel and confirm DNS name and local port are correct.
Defensive patterns

Strategy: retry

Validate before calling

// confirm tailscale funnel is registered before relying on the URL
import { execSync } from "child_process";
const out = execSync("tailscale funnel status", { encoding: "utf8" });
if (!out.includes("https://")) throw new Error("funnel not active yet");

Try / catch

// treat only timeout as retryable, mirroring the library
try {
  const res = await enableTailscale();
} catch (e) {
  if (e.message.startsWith("Health check timeout")) scheduleRetry();
  else throw e;
}

Prevention

When it happens

Trigger: waitForHealth(result.tunnelUrl, token) times out after tailscale funnel is configured: slow cert provisioning, DNS over Tailscale still settling, funnel just started, or MagicDNS name not yet resolvable within the timeout window.

Common situations: First-time funnel setup on a fresh machine; tailscaled slow to propagate the funnel; large network latency to the node; cert command just ran; temporary tailscaled restart.

Understand the failure class

Related errors


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