{"record":{"id":"67315cb9835b74b0","repo":"juanfont/headscale","slug":"waiting-for-headscale-w","errorCode":null,"errorMessage":"waiting for headscale: %w","messagePattern":"waiting for headscale: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/dev/main.go","lineNumber":158,"sourceCode":"\n\t// Start headscale serve.\n\tfmt.Println(\"Starting headscale server...\")\n\n\tserve := exec.CommandContext(ctx, hsBin, \"serve\", \"-c\", configPath)\n\tserve.Stdout = os.Stdout\n\tserve.Stderr = os.Stderr\n\n\terr = serve.Start()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"starting headscale: %w\", err)\n\t}\n\n\t// Wait for server to be ready.\n\thealthURL := fmt.Sprintf(\"http://127.0.0.1:%d/health\", *port)\n\n\terr = waitForHealth(ctx, healthURL, 30*time.Second)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"waiting for headscale: %w\", err)\n\t}\n\n\t// Create user.\n\tfmt.Println(\"Creating user and pre-auth key...\")\n\n\tuserJSON, err := runHS(ctx, hsBin, configPath, \"users\", \"create\", \"dev\", \"-o\", \"json\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating user: %w\", err)\n\t}\n\n\tuserID, err := extractUserID(userJSON)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parsing user: %w\", err)\n\t}\n\n\t// Create pre-auth key.\n\tkeyJSON, err := runHS(\n\t\tctx, hsBin, configPath,","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/cmd/dev/main.go#L140-L176","documentation":"\"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).","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before (port 8080 occupied)\ngo run ./cmd/dev\n\n# after\ngo run ./cmd/dev -port 8090","handlingStrategy":"retry","validationCode":"// pre-flight: is anything already on the port?\nln, err := net.Listen(\"tcp\", fmt.Sprintf(\"127.0.0.1:%d\", *port))\nif err != nil {\n\treturn fmt.Errorf(\"port %d busy: pick another with -port\", *port)\n}\nln.Close()","typeGuard":null,"tryCatchPattern":"if err := waitForHealth(ctx, healthURL, 30*time.Second); err != nil {\n\tif ctx.Err() != nil {\n\t\treturn nil // Ctrl+C during startup\n\t}\n\t// child's own startup error is on stdout; check it before retrying\n\t// retry once with a different port or after freeing resources\n}","preventionTips":["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"],"tags":["dev-tooling","health-check","startup","timeout"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}