gastownhall/beads · error
max retries (%d) exceeded: %w
Error message
max retries (%d) exceeded: %w
What it means
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.
Source
Thrown at internal/jira/client.go:438
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, ctx.Err()
case <-time.After(delay):
continue
}
}
return nil, fmt.Errorf("jira API returned %d: %s", resp.StatusCode, string(respBody))
}
return nil, fmt.Errorf("max retries (%d) exceeded: %w", MaxRetries+1, lastErr)
}
// setAuth sets the appropriate authentication header on the request.
func (c *Client) setAuth(req *http.Request) {
isCloud := strings.Contains(c.URL, "atlassian.net")
if (isCloud || c.Username != "") && c.Username != "" {
auth := base64.StdEncoding.EncodeToString([]byte(c.Username + ":" + c.APIToken))
req.Header.Set("Authorization", "Basic "+auth)
} else {
req.Header.Set("Authorization", "Bearer "+c.APIToken)
}
}
// DescriptionToPlainText extracts plain text from Jira's ADF (Atlassian Document Format).
// Jira v3 API returns descriptions as ADF JSON, not plain text.
func DescriptionToPlainText(raw json.RawMessage) string {
if len(raw) == 0 || string(raw) == "null" {
return ""View on GitHub (pinned to 71377f2769)
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.
Example fix
// before: immediate retry on exhaustion defeats the purpose
for { issues, err := search(); if err != nil { continue } }
// after: exponential backoff around the already-retrying client
if err != nil && strings.Contains(err.Error(), "max retries") {
time.Sleep(30 * time.Second)
issues, err = search()
} Defensive patterns
Strategy: retry
Validate before calling
// Confirm the instance is up before invoking the client
resp, err := http.Get(jiraURL + "/status")
if err != nil || (resp != nil && resp.StatusCode >= 500) {
return fmt.Errorf("jira unavailable, skipping run")
} Try / catch
if err != nil {
if strings.Contains(err.Error(), "max retries") {
cause := errors.Unwrap(err) // lastErr: status or read error
log.Printf("jira exhausted retries, last cause: %v", cause)
time.Sleep(60 * time.Second) // long backoff; client already retried internally
return op()
}
return err
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- failed to read response (attempt %d/%d): %w
- transient error %d (attempt %d/%d)
- transient error %d (attempt %d/%d)
- request failed (attempt %d/%d): %w
- fetch issue %s: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/d8782654fca302ea.
Report an issue: GitHub.