gastownhall/beads · error
max retries (%d) exceeded: %w
Error message
max retries (%d) exceeded: %w
What it means
After MaxRetries+1 attempts, every attempt failed with a transient error (network failure, body read failure, or retryable status). doRequest returns this final error wrapping the last underlying error (lastErr). The caller sees both the retry budget exhausted and the root cause in the chain.
Source
Thrown at internal/gitlab/client.go:188
if !useServerDelay {
if half := int64(delay / 2); half > 0 {
delay += time.Duration(rand.Int64N(half)) //nolint:gosec // G404: jitter for retry backoff does not need crypto rand
}
}
lastErr = fmt.Errorf("transient error %d (attempt %d/%d)", resp.StatusCode, attempt+1, MaxRetries+1)
select {
case <-ctx.Done():
return nil, nil, ctx.Err()
case <-time.After(delay):
continue
}
}
return nil, nil, fmt.Errorf("API error: %s (status %d)", string(respBody), resp.StatusCode)
}
return nil, nil, fmt.Errorf("max retries (%d) exceeded: %w", MaxRetries+1, lastErr)
}
// applyFilter adds IssueFilter fields as query parameters to the params map.
// ProjectID filtering is done client-side (not supported by GitLab API on group endpoints).
func applyFilter(params map[string]string, filter *IssueFilter) {
if filter == nil {
return
}
if filter.Labels != "" {
params["labels"] = filter.Labels
}
if filter.Milestone != "" {
params["milestone"] = filter.Milestone
}
if filter.Assignee != "" {
params["assignee_username"] = filter.Assignee
}
}View on GitHub (pinned to 71377f2769)
Solutions
- Inspect errors.Unwrap / %v of the error to see the last root cause (connection refused, 429, read failure)
- Wait for the outage/rate-limit window to pass and resume with backoff; don't tight-loop retries
- Increase MaxRetries or add caller-level exponential backoff for known flaky networks
- Monitor GitLab health/status page for your instance before debugging app code
Example fix
// before
issues, _, err := client.ListIssues(ctx, f)
if err != nil { return err } // opaque 'max retries exceeded'
// after
if err != nil { return fmt.Errorf("list issues: %w", err) } // preserve chain; log with %+v to see root cause Defensive patterns
Strategy: retry
Validate before calling
if err := pingGitLab(base, token); err != nil {
return fmt.Errorf("GitLab unreachable before sync: %w", err)
} Type guard
func isRetryExhausted(err error) bool {
return strings.Contains(err.Error(), "max retries")
} Try / catch
if err != nil {
if isRetryExhausted(err) {
log.Printf("GitLab down or rate-limited; root cause: %v", err)
time.Sleep(5 * time.Minute) // resume later
}
return err
} Prevention
- Log with %+v to keep the wrapped root cause visible
- Add circuit-breaker logic instead of immediate re-looping after exhaustion
- Size MaxRetries/backoff to your instance's realistic outage windows
- Alert on GitLab status so on-call knows the failure is upstream
When it happens
Trigger: Sustained network outage, persistent 429/5xx across the full retry window, or repeated mid-body read failures; any client API call (list/create/update issues) during that window returns this after the retry loop.
Common situations: GitLab maintenance window longer than the retry backoff; VPN/proxy down for the whole retry duration; hard rate-limit (429) persisting because the app keeps hammering after the error returns.
Related errors
- request failed (attempt %d/%d): %w
- failed to read response (attempt %d/%d): %w
- server not reachable: %w
- git ls-remote %s failed: %s: %w
- failed after %d retries: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/d81dbdf94248f304.
Report an issue: GitHub.