juanfont/headscale · error
waiting for headscale: %w
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).
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)
Solutions
- Check the console for the child's own startup error (port in use, bad config) and fix that
- Free the port: `lsof -i :<port>` then stop the conflicting process, or run cmd/dev with a different -port
- 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
- Pick a dedicated port for cmd/dev and keep it free
- Check the child's console output first — the timeout is usually a symptom, not the cause
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
- creating request: %w
- reaching headscale container: %w
- STUN address not set
- initial DERPMap is empty, Headscale requires at least one en
- database type not supported
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/67315cb9835b74b0.
Report an issue: GitHub.