gastownhall/beads · error

max retries (%d) exceeded: %w

Error message

max retries (%d) exceeded: %w

What it means

doRequest exhausted all retry attempts against the Azure DevOps REST API without getting a non-transient response. The final transient status error (lastErr, see 1407) is wrapped via %w so callers can errors.Unwrap/errors.As into it. Every API method (FetchWorkItems, WIQL queries, work-item create/update/link operations) funnels through doRequest, so any of them can produce this.

Source

Thrown at internal/ado/client.go:286

			// Only add jitter to our own exponential backoff, not server-mandated delays
			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, maxAttempts+1)
			select {
			case <-ctx.Done():
				return nil, ctx.Err()
			case <-time.After(delay):
				continue
			}
		}

		return nil, &APIError{StatusCode: resp.StatusCode, Body: string(respBody)}
	}

	return nil, fmt.Errorf("max retries (%d) exceeded: %w", maxAttempts+1, lastErr)
}

// addAPIVersion appends the api-version query parameter to a URL string.
func addAPIVersion(urlStr string) string {
	if strings.Contains(urlStr, "?") {
		return urlStr + "&api-version=" + APIVersion
	}
	return urlStr + "?api-version=" + APIVersion
}

// listResponse is a generic envelope for ADO list API responses.
type listResponse struct {
	Count int             `json:"count"`
	Value json.RawMessage `json:"value"`
}

// escapeWIQL escapes a string for safe inclusion in a WIQL query literal.
func escapeWIQL(s string) string {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped cause (`errors.Unwrap` or %v of the error) to see the last status code and target accordingly (429 vs 503).
  2. Increase backoff between your own calls or reduce concurrency/batch size to avoid rate limits.
  3. Increase the client's retry budget or your operation timeout if the deadline truncates retries.
  4. Check Azure DevOps status page for a service incident; wait and retry.
  5. Verify network/proxy configuration if 5xx comes from an intermediate hop.

Example fix

// before
items, err := client.FetchWorkItems(ctx, ids) // fails under sustained 429
if err != nil { return err }
// after
items, err := client.FetchWorkItems(ctx, ids)
if err != nil {
    var apiErr *ado.APIError
    if errors.As(err, &apiErr) && apiErr.StatusCode == 429 {
        time.Sleep(retryAfterOr(30*time.Second)); items, err = client.FetchWorkItems(ctx, ids)
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// pre-flight: verify PAT validity and org reachability before long batch jobs
resp, _ := http.Get(orgURL + "/_apis/projects?api-version=" + ado.APIVersion)
if resp.StatusCode != 200 { /* abort early: auth or org misconfigured */ }

Try / catch

items, err := client.FetchWorkItems(ctx, ids)
if err != nil {
    var apiErr *ado.APIError
    if errors.As(err, &apiErr) && apiErr.StatusCode == 429 {
        // schedule retry with long backoff
    } else {
        return fmt.Errorf("non-retryable: %w", err)
    }
}

Prevention

When it happens

Trigger: Any ADO API call where every attempt (maxAttempts+1) returned a retryable status (429/5xx) or network-level transient failure. Raised after the retry loop completes with lastErr non-nil and no successful response or non-retryable APIError.

Common situations: Sustained rate limiting from bulk imports/syncs; prolonged Azure DevOps outage; network partitions between CI and dev.azure.com; misconfigured proxy returning 5xx; context timeouts shorter than total backoff window.

Related errors


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