gastownhall/beads · error
max retries (%d) exceeded: %w
Error message
max retries (%d) exceeded: %w
What it means
executeOnce retries transient failures (network errors, unreadable bodies, and 429 rate limits) up to MaxRetries+1 attempts; when all attempts fail it wraps the last error with this message. It means the Linear API was never successfully reached or kept returning a retryable failure for the entire retry window.
Source
Thrown at internal/linear/client.go:436
Data json.RawMessage `json:"data"`
Errors []GraphQLError `json:"errors,omitempty"`
}
if err := json.Unmarshal(respBody, &gqlResp); err != nil {
return nil, lastStatus, fmt.Errorf("failed to parse response: %w (body: %s)", err, string(respBody))
}
if len(gqlResp.Errors) > 0 {
errMsgs := make([]string, len(gqlResp.Errors))
for i, e := range gqlResp.Errors {
errMsgs[i] = e.Message
}
return nil, lastStatus, fmt.Errorf("GraphQL errors: %s", strings.Join(errMsgs, "; "))
}
return gqlResp.Data, lastStatus, nil
}
return nil, lastStatus, fmt.Errorf("max retries (%d) exceeded: %w", MaxRetries+1, lastErr)
}
// FetchIssues retrieves issues from Linear with optional filtering by state.
// state can be: "open" (unstarted/started), "closed" (completed/canceled), or "all".
// If ProjectID is set on the client, only issues from that project are returned.
func (c *Client) FetchIssues(ctx context.Context, state string) ([]Issue, error) {
var allIssues []Issue
var cursor string
filter := map[string]interface{}{
"team": map[string]interface{}{
"id": map[string]interface{}{
"eq": c.TeamID,
},
},
}
// Add project filter if configuredView on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped lastErr after the colon — it names the root cause (rate limit vs network)
- If rate limited: reduce call frequency, raise linear.rate_limit_floor awareness, or schedule syncs further apart
- If network: verify connectivity to the Linear endpoint and proxy settings
- Increase the context deadline so retries aren't cut off by cancellation
- Check https://status.linear.app for an ongoing outage before further debugging
Example fix
// before: one tight loop with a short timeout
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
// after: give retries room to work
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
issues, err := client.FetchIssues(ctx, "open")
if err != nil && strings.Contains(err.Error(), "max retries") {
log.Printf("Linear unreachable after retries: %v", err) // inspect wrapped cause
} Defensive patterns
Strategy: retry
Validate before calling
// Before calling, ensure a generous deadline so internal retries can complete ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel()
Try / catch
// detect retry exhaustion and re-run with a longer backoff
_, err := client.Execute(ctx, req)
if err != nil && strings.Contains(err.Error(), "max retries (") {
time.Sleep(30 * time.Second) // or requeue the job
_, err = client.Execute(ctx, req)
} Prevention
- Use context timeouts well larger than MaxRetries × backoff
- Space out bulk syncs to avoid 429 storms
- Monitor X-RateLimit-Requests-Remaining headers
- Alert on Linear status page incidents
- Check network/proxy reachability to the endpoint at startup
When it happens
Trigger: Client.Execute → executeOnce when every attempt fails: persistent 429 rate-limiting without a recoverable Retry-After, repeated network timeouts/DNS failures, or context cancellation mid-backoff (lastErr will be a 'rate limited (attempt N/M)' or 'request failed (attempt N/M)' wrapper).
Common situations: Bursty sync jobs exhausting Linear's rate limit; corporate proxy or VPN blocking api.linear.ly; very short context timeouts that expire during backoff sleeps; Linear status incidents.
Related errors
- failed after %d retries: %w
- fetching issues from team %s: %w
- transient error %d (attempt %d/%d)
- request failed (attempt %d/%d): %w
- failed to read response (attempt %d/%d): %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/6a9e9a13e11d1160.
Report an issue: GitHub.