{"record":{"id":"cb32b63316481854","repo":"gastownhall/beads","slug":"max-retries-d-exceeded-w","errorCode":null,"errorMessage":"max retries (%d) exceeded: %w","messagePattern":"max retries \\((.+?)\\) exceeded: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/ado/client.go","lineNumber":286,"sourceCode":"\t\t\t// Only add jitter to our own exponential backoff, not server-mandated delays\n\t\t\tif !useServerDelay {\n\t\t\t\tif half := int64(delay / 2); half > 0 {\n\t\t\t\t\tdelay += time.Duration(rand.Int64N(half)) //nolint:gosec // G404: jitter for retry backoff does not need crypto rand\n\t\t\t\t}\n\t\t\t}\n\t\t\tlastErr = fmt.Errorf(\"transient error %d (attempt %d/%d)\", resp.StatusCode, attempt+1, maxAttempts+1)\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, ctx.Err()\n\t\t\tcase <-time.After(delay):\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\treturn nil, &APIError{StatusCode: resp.StatusCode, Body: string(respBody)}\n\t}\n\n\treturn nil, fmt.Errorf(\"max retries (%d) exceeded: %w\", maxAttempts+1, lastErr)\n}\n\n// addAPIVersion appends the api-version query parameter to a URL string.\nfunc addAPIVersion(urlStr string) string {\n\tif strings.Contains(urlStr, \"?\") {\n\t\treturn urlStr + \"&api-version=\" + APIVersion\n\t}\n\treturn urlStr + \"?api-version=\" + APIVersion\n}\n\n// listResponse is a generic envelope for ADO list API responses.\ntype listResponse struct {\n\tCount int             `json:\"count\"`\n\tValue json.RawMessage `json:\"value\"`\n}\n\n// escapeWIQL escapes a string for safe inclusion in a WIQL query literal.\nfunc escapeWIQL(s string) string {","sourceCodeStart":268,"sourceCodeEnd":304,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/ado/client.go#L268-L304","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Read the wrapped cause (`errors.Unwrap` or %v of the error) to see the last status code and target accordingly (429 vs 503).","Increase backoff between your own calls or reduce concurrency/batch size to avoid rate limits.","Increase the client's retry budget or your operation timeout if the deadline truncates retries.","Check Azure DevOps status page for a service incident; wait and retry.","Verify network/proxy configuration if 5xx comes from an intermediate hop."],"exampleFix":"// before\nitems, err := client.FetchWorkItems(ctx, ids) // fails under sustained 429\nif err != nil { return err }\n// after\nitems, err := client.FetchWorkItems(ctx, ids)\nif err != nil {\n    var apiErr *ado.APIError\n    if errors.As(err, &apiErr) && apiErr.StatusCode == 429 {\n        time.Sleep(retryAfterOr(30*time.Second)); items, err = client.FetchWorkItems(ctx, ids)\n    }\n}","handlingStrategy":"retry","validationCode":"// pre-flight: verify PAT validity and org reachability before long batch jobs\nresp, _ := http.Get(orgURL + \"/_apis/projects?api-version=\" + ado.APIVersion)\nif resp.StatusCode != 200 { /* abort early: auth or org misconfigured */ }","typeGuard":null,"tryCatchPattern":"items, err := client.FetchWorkItems(ctx, ids)\nif err != nil {\n    var apiErr *ado.APIError\n    if errors.As(err, &apiErr) && apiErr.StatusCode == 429 {\n        // schedule retry with long backoff\n    } else {\n        return fmt.Errorf(\"non-retryable: %w\", err)\n    }\n}","preventionTips":["Add an outer retry-with-long-backoff around library calls for batch jobs.","Cap concurrency against ADO to stay under rate limits.","Set context deadlines longer than the client's total backoff window.","Subscribe to Azure DevOps service health before running large migrations.","Log the wrapped cause (%v) — it reveals the decisive status code."],"tags":["http","azure-devops","retry-exhausted","rate-limit"],"backgroundTag":"retry-limit-exceeded","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}