juanfont/headscale · error

waiting for headscale

Error message

waiting for headscale: %w

What it means

"waiting for headscale: %w" at cmd/dev/main.go:158 wraps waitForHealth(ctx, "http://127.0.0.1:<port>/health", 30s), which polls the health endpoint until it returns 200 or the 30-second timeout expires. The error means the server did not become healthy in time: either the child exited/crashed during startup (its logs are on the console), the port is wrong, or the timeout was too short for a slow first boot (e.g. Let's Encrypt, DB init).

Solutions

  1. Check the console for the child's own startup error (port in use, bad config) and fix that
  2. Free the port: `lsof -i :<port>` then stop the conflicting process, or run cmd/dev with a different -port
  3. Re-run once the machine is less loaded — the 30s window is fixed in the tool

Example fix

# before (port 8080 occupied)
go run ./cmd/dev

# after
go run ./cmd/dev -port 8090
Defensive patterns

Strategy: retry

Validate before calling

// pre-flight: is anything already on the port?
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", *port))
if err != nil {
	return fmt.Errorf("port %d busy: pick another with -port", *port)
}
ln.Close()

Try / catch

if err := waitForHealth(ctx, healthURL, 30*time.Second); err != nil {
	if ctx.Err() != nil {
		return nil // Ctrl+C during startup
	}
	// child's own startup error is on stdout; check it before retrying
	// retry once with a different port or after freeing resources
}

Prevention

When it happens

Trigger: Running cmd/dev when the spawned server fails during startup (config error, port <port> already in use, database migration failure); the machine is slow enough that 30s elapses before /health answers 200; ctx cancelled via Ctrl+C during the wait.

Common situations: Another headscale or dev instance already listening on the chosen port (use -port to pick another); startup blocking on DNS/TLS; overloaded dev machine or CI runner.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/67315cb9835b74b0. Report an issue: GitHub.

Appendix: source

Thrown at cmd/dev/main.go:158

	// Start headscale serve.
	fmt.Println("Starting headscale server...")

	serve := exec.CommandContext(ctx, hsBin, "serve", "-c", configPath)
	serve.Stdout = os.Stdout
	serve.Stderr = os.Stderr

	err = serve.Start()
	if err != nil {
		return fmt.Errorf("starting headscale: %w", err)
	}

	// Wait for server to be ready.
	healthURL := fmt.Sprintf("http://127.0.0.1:%d/health", *port)

	err = waitForHealth(ctx, healthURL, 30*time.Second)
	if err != nil {
		return fmt.Errorf("waiting for headscale: %w", err)
	}

	// Create user.
	fmt.Println("Creating user and pre-auth key...")

	userJSON, err := runHS(ctx, hsBin, configPath, "users", "create", "dev", "-o", "json")
	if err != nil {
		return fmt.Errorf("creating user: %w", err)
	}

	userID, err := extractUserID(userJSON)
	if err != nil {
		return fmt.Errorf("parsing user: %w", err)
	}

	// Create pre-auth key.
	keyJSON, err := runHS(
		ctx, hsBin, configPath,

View on GitHub (pinned to 565fd254d0)