gastownhall/beads · warning
transient error %d (attempt %d/%d)
Error message
transient error %d (attempt %d/%d)
What it means
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.
Source
Thrown at internal/jira/client.go:426
delay := RetryDelay * time.Duration(1<<uint(attempt))
useServerDelay := false
// Use Retry-After header if present (no jitter — respect server-mandated delay)
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
delay = time.Duration(seconds) * time.Second
useServerDelay = true
}
}
// Only add jitter to our own exponential backoff, not server-mandated delays
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 != "" {View on GitHub (pinned to 71377f2769)
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.
Example fix
// before: tight loop hammering Jira
for _, key := range keys { fetch(key) }
// after: pace requests to avoid 429s
for _, key := range keys {
fetch(key)
time.Sleep(200 * time.Millisecond)
} Defensive patterns
Strategy: retry
Try / catch
if err != nil && strings.Contains(err.Error(), "transient error") {
// only meaningful when surfaced via 'max retries exceeded'
status := extractStatus(err.Error()) // e.g. 429 vs 503
if status == 429 {
scheduleAfter(rateLimitReset)
} else {
scheduleAfter(30 * time.Second)
}
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- transient error %d (attempt %d/%d)
- failed to read response (attempt %d/%d): %w
- max retries (%d) exceeded: %w
- Aborting push to %s: provider rate limit hit (%s); %d issue(
- max retries (%d) exceeded: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/cd4273f8df14c171.
Report an issue: GitHub.