juanfont/headscale · error
timeout waiting for new client: %w
Error message
timeout waiting for new client: %w
What it means
Final wrapping error from Scenario.AddAndLoginClient when backoff.Retry exhausts its 10s budget (constant 500ms backoff) without the new client appearing. The %w chain preserves whichever sub-error (list failure, count mismatch, or FindNewClient failure) occurred on the last attempt, making it the umbrella error for 'new node never showed up'.
Source
Thrown at integration/helpers.go:1184
_, err = backoff.Retry(t.Context(), func() (struct{}, error) {
updatedClients, err := s.ListTailscaleClients(username)
if err != nil {
return struct{}{}, fmt.Errorf("listing updated clients: %w", err)
}
if len(updatedClients) != len(originalClients)+1 {
return struct{}{}, fmt.Errorf("expected %d clients, got %d", len(originalClients)+1, len(updatedClients)) //nolint:err113
}
newClient, err = FindNewClient(originalClients, updatedClients)
if err != nil {
return struct{}{}, fmt.Errorf("finding new client: %w", err)
}
return struct{}{}, nil
}, backoff.WithBackOff(backoff.NewConstantBackOff(500*time.Millisecond)), backoff.WithMaxElapsedTime(10*time.Second))
if err != nil {
return nil, fmt.Errorf("timeout waiting for new client: %w", err)
}
// Get the user and create preauth key
user, err := GetUserByName(headscale, username)
if err != nil {
return nil, fmt.Errorf("getting user: %w", err)
}
authKey, err := s.CreatePreAuthKey(mustParseID(user.Id), true, false)
if err != nil {
return nil, fmt.Errorf("creating preauth key: %w", err)
}
// Login the new client
err = newClient.Login(headscale.GetEndpoint(), authKey.Key)
if err != nil {
return nil, fmt.Errorf("logging in new client: %w", err)
}View on GitHub (pinned to 565fd254d0)
Solutions
- Inspect the wrapped cause in the error chain — it names the actual failing step (listing, count, or find)
- Check both headscale and the new tailscale container logs under control_logs/<runID>/
- On consistently slow hosts, increase backoff.WithMaxElapsedTime in integration/helpers.go
- Run `go run ./cmd/hi doctor` to rule out environment problems
Example fix
// before _, backoff.WithMaxElapsedTime(10*time.Second)) // after (slow CI host) _, backoff.WithMaxElapsedTime(30*time.Second))
Defensive patterns
Strategy: retry
Try / catch
// Unwrap to find the real cause
client, err := scenario.AddAndLoginClient(t, user, ver)
if err != nil {
var to *backoff.Error // or inspect err chain text
_ = to
t.Fatalf("add+login failed: %v", err)
} Prevention
- On slow CI, raise backoff.WithMaxElapsedTime in integration/helpers.go
- Read the wrapped cause — it distinguishes listing vs count vs diff failures
- Check both headscale and node container logs before assuming a code bug
When it happens
Trigger: Any of errors 660/661/662 persisting for the full backoff.WithMaxElapsedTime(10*time.Second) window: node never registers, listing always fails, or the diff never resolves to exactly one new client.
Common situations: Overloaded CI runner where container startup plus registration exceeds 10s; headscale container unhealthy so registration always fails; incompatible tailscale/headscale version pairing.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- listing updated clients: %w
- expected %d clients, got %d
- registration expired
- waiting for headscale: %w
- dumping config: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/754a15c92a0b3694.
Report an issue: GitHub.