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

  1. Inspect errors.Unwrap / %v of the error to see the last root cause (connection refused, 429, read failure)
  2. Wait for the outage/rate-limit window to pass and resume with backoff; don't tight-loop retries
  3. Increase MaxRetries or add caller-level exponential backoff for known flaky networks
  4. 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

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


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