{"record":{"id":"d8782654fca302ea","repo":"gastownhall/beads","slug":"max-retries-d-exceeded-w-d87826","errorCode":null,"errorMessage":"max retries (%d) exceeded: %w","messagePattern":"max retries \\((.+?)\\) exceeded: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/jira/client.go","lineNumber":438,"sourceCode":"\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\n\t\t\tlastErr = fmt.Errorf(\"transient error %d (attempt %d/%d)\", resp.StatusCode, attempt+1, MaxRetries+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, fmt.Errorf(\"jira API returned %d: %s\", resp.StatusCode, string(respBody))\n\t}\n\n\treturn nil, fmt.Errorf(\"max retries (%d) exceeded: %w\", MaxRetries+1, lastErr)\n}\n\n// setAuth sets the appropriate authentication header on the request.\nfunc (c *Client) setAuth(req *http.Request) {\n\tisCloud := strings.Contains(c.URL, \"atlassian.net\")\n\tif (isCloud || c.Username != \"\") && c.Username != \"\" {\n\t\tauth := base64.StdEncoding.EncodeToString([]byte(c.Username + \":\" + c.APIToken))\n\t\treq.Header.Set(\"Authorization\", \"Basic \"+auth)\n\t} else {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.APIToken)\n\t}\n}\n\n// DescriptionToPlainText extracts plain text from Jira's ADF (Atlassian Document Format).\n// Jira v3 API returns descriptions as ADF JSON, not plain text.\nfunc DescriptionToPlainText(raw json.RawMessage) string {\n\tif len(raw) == 0 || string(raw) == \"null\" {\n\t\treturn \"\"","sourceCodeStart":420,"sourceCodeEnd":456,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/jira/client.go#L420-L456","documentation":"This is the terminal error of doRequest: after MaxRetries+1 total attempts all ending in retriable failures (read errors or retriable statuses), the client gives up and returns 'max retries exceeded' wrapping the last attempt's error (lastErr). Callers should treat it as a transient-availability problem and retry later with their own backoff.","triggerScenarios":"All MaxRetries+1 attempts of doRequest fail with retriable conditions (429/5xx statuses or response-body read errors); lastErr from the final attempt is wrapped into this message.","commonSituations":"Sustained Jira outage or maintenance window longer than the total backoff; persistent rate limiting from a bulk sync; unstable network/VPN across all attempts; self-hosted instance down.","solutions":["Wait and retry later with your own longer backoff — the condition is transient by definition.","Check Atlassian status page or your instance's health before retrying.","Reduce request volume if lastErr indicates 429 rate limiting.","Unwrap lastErr (errors.Unwrap / %w cause) to see the concrete final failure (status code or read error).","Increase MaxRetries or the backoff base in client configuration if your workload routinely hits this."],"exampleFix":"// before: immediate retry on exhaustion defeats the purpose\nfor { issues, err := search(); if err != nil { continue } }\n// after: exponential backoff around the already-retrying client\nif err != nil && strings.Contains(err.Error(), \"max retries\") {\n    time.Sleep(30 * time.Second)\n    issues, err = search()\n}","handlingStrategy":"retry","validationCode":"// Confirm the instance is up before invoking the client\nresp, err := http.Get(jiraURL + \"/status\")\nif err != nil || (resp != nil && resp.StatusCode >= 500) {\n    return fmt.Errorf(\"jira unavailable, skipping run\")\n}","typeGuard":null,"tryCatchPattern":"if err != nil {\n    if strings.Contains(err.Error(), \"max retries\") {\n        cause := errors.Unwrap(err) // lastErr: status or read error\n        log.Printf(\"jira exhausted retries, last cause: %v\", cause)\n        time.Sleep(60 * time.Second) // long backoff; client already retried internally\n        return op()\n    }\n    return err\n}","preventionTips":["Do not wrap the client in tight outer retry loops — add long, exponential outer backoff.","Schedule heavy syncs outside maintenance windows.","Alert on this error: it means MaxRetries+1 attempts all failed.","Reduce request rate if lastErr shows 429.","Size MaxRetries/backoff to your worst-case transient outage length."],"tags":["jira","http","retry","exhaustion","availability"],"backgroundTag":"max-retries-exceeded","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}