{"record":{"id":"cd4273f8df14c171","repo":"gastownhall/beads","slug":"transient-error-d-attempt-d-d-cd4273","errorCode":null,"errorMessage":"transient error %d (attempt %d/%d)","messagePattern":"transient error (.+?) \\(attempt (.+?)/(.+?)\\)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/jira/client.go","lineNumber":426,"sourceCode":"\t\t\tdelay := RetryDelay * time.Duration(1<<uint(attempt))\n\t\t\tuseServerDelay := false\n\n\t\t\t// Use Retry-After header if present (no jitter — respect server-mandated delay)\n\t\t\tif retryAfter := resp.Header.Get(\"Retry-After\"); retryAfter != \"\" {\n\t\t\t\tif seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {\n\t\t\t\t\tdelay = time.Duration(seconds) * time.Second\n\t\t\t\t\tuseServerDelay = true\n\t\t\t\t}\n\t\t\t}\n\n\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\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 != \"\" {","sourceCodeStart":408,"sourceCodeEnd":444,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/jira/client.go#L408-L444","documentation":"doRequest records this as lastErr when a retriable status (429 rate limit, 500, 502, 503, etc.) is received on a given attempt. The client sleeps with exponential backoff plus jitter (jitter disabled when the server provides a Retry-After delay via useServerDelay) and retries. It is normally only observed wrapped inside 'max retries (%d) exceeded' after all MaxRetries+1 attempts fail.","triggerScenarios":"resp.StatusCode is in the retriable set (429, 500, 502, and other 5xx per the retriable check) during any attempt; the message carries the status code and attempt number (of MaxRetries+1).","commonSituations":"Bulk syncs or tight loops tripping Jira Cloud rate limits (429); Jira instance overloaded or deploying (5xx); Atlassian incident; shared Cloud instance throttling heavy JQL searches.","solutions":["Respect the backoff — if all retries failed, wait and rerun the operation later rather than immediately retrying in a loop.","Reduce request concurrency/batch size; add delays between bulk operations.","For 429: honor Retry-After (the client already does when the server sends it) and spread work over time.","Check the Atlassian status page or your instance health for ongoing incidents.","Cache Jira responses to cut request volume if you repeatedly fetch the same data."],"exampleFix":"// before: tight loop hammering Jira\nfor _, key := range keys { fetch(key) }\n// after: pace requests to avoid 429s\nfor _, key := range keys {\n    fetch(key)\n    time.Sleep(200 * time.Millisecond)\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"transient error\") {\n    // only meaningful when surfaced via 'max retries exceeded'\n    status := extractStatus(err.Error()) // e.g. 429 vs 503\n    if status == 429 {\n        scheduleAfter(rateLimitReset)\n    } else {\n        scheduleAfter(30 * time.Second)\n    }\n}","preventionTips":["Throttle bulk syncs; add per-request delays or worker limits.","Respect Retry-After headers rather than fixed intervals.","Cache frequently read Jira entities to reduce call volume.","Monitor Atlassian status pages during heavy operations.","Use exponential backoff with jitter in any outer retry loop."],"tags":["jira","http","rate-limit","retry","backoff"],"backgroundTag":"http-429-rate-limited","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}