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

  1. Wait and retry later with your own longer backoff — the condition is transient by definition.
  2. Check Atlassian status page or your instance's health before retrying.
  3. Reduce request volume if lastErr indicates 429 rate limiting.
  4. Unwrap lastErr (errors.Unwrap / %w cause) to see the concrete final failure (status code or read error).
  5. 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

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


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