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

  1. Respect the backoff — if all retries failed, wait and rerun the operation later rather than immediately retrying in a loop.
  2. Reduce request concurrency/batch size; add delays between bulk operations.
  3. For 429: honor Retry-After (the client already does when the server sends it) and spread work over time.
  4. Check the Atlassian status page or your instance health for ongoing incidents.
  5. 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

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


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