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 configured

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped lastErr after the colon — it names the root cause (rate limit vs network)
  2. If rate limited: reduce call frequency, raise linear.rate_limit_floor awareness, or schedule syncs further apart
  3. If network: verify connectivity to the Linear endpoint and proxy settings
  4. Increase the context deadline so retries aren't cut off by cancellation
  5. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/6a9e9a13e11d1160. Report an issue: GitHub.