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
- Wait — the watchdog retries the health check automatically; verify reachability shortly after.
- Verify 'tailscale funnel status' / 'tailscale status' shows the funnel active and the hostname correct.
- Check that tailscaled is running and the device is connected to the tailnet.
- 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
- Ensure tailscaled is running and connected before enabling the funnel.
- Allow extra time on first-time funnel setup (cert + DNS settling).
- Verify the funnel hostname matches your tailnet DNS name.
- Poll the tunnel URL yourself for a minute after enabling instead of trusting immediately.
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Health check timeout after ${HEALTH_CHECK.timeoutMs}ms
- Health check timeout after ${HEALTH_CHECK.timeoutMs}ms
- cancelled
- cancelled
- tailscale cancelled
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/f29041d164df8c0e.
Report an issue: GitHub.