gastownhall/beads · warning · RateLimitedError

Aborting push to %s: provider rate limit hit (%s); %d issue(

Error message

Aborting push to %s: provider rate limit hit (%s); %d issue(s) skipped — retry after the cooldown

What it means

During a push, when the provider API returns a rate-limit error (an error implementing tracker.RateLimitedError), the engine aborts the remaining queue rather than hammering the API, warns with this message, and leaves the remaining issues for the next sync. The message includes the provider name, the retry-after hint (or 'unknown' if the server didn't say), and how many issues were skipped.

Source

Thrown at internal/tracker/errors.go:27

// RateLimitedError is implemented by provider errors when the upstream API
// has rate-limited the request. The push loop uses errors.As against this
// interface to detect rate limiting without importing any provider package.
type RateLimitedError interface {
	error
	// RateLimitRetryAfter returns the wait before retrying. Zero means
	// "the server didn't say".
	RateLimitRetryAfter() time.Duration
}

func isRateLimitedErr(err error) bool {
	var rl RateLimitedError
	return errors.As(err, &rl)
}

// warnRateLimitAbort emits the standard "we hit a provider rate limit, the
// rest of the queue is left for next sync" message.
func (e *Engine) warnRateLimitAbort(err error, remaining int) {
	e.warn("Aborting push to %s: provider rate limit hit (%s); %d issue(s) skipped — retry after the cooldown",
		e.Tracker.DisplayName(), formatRateLimitWait(err), remaining)
}

func formatRateLimitWait(err error) string {
	var rl RateLimitedError
	if !errors.As(err, &rl) {
		return "unknown"
	}
	d := rl.RateLimitRetryAfter()
	if d <= 0 {
		return "unknown"
	}
	return fmt.Sprintf("retry after %s", d.Round(time.Second))
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Wait for the cooldown shown in the message (RateLimitRetryAfter), then re-run the push; the remaining issues will be retried.
  2. Use a token with a higher rate limit or dedicated to this automation (avoid shared CI tokens).
  3. Reduce batch size / throttle pushes (push in smaller batches spaced over time).
  4. Ensure the provider client is authenticating — anonymous limits are far lower.
  5. If the provider reports 'unknown' wait, check its dashboard or Retry-After headers for the actual reset time.

Example fix

// before: bulk push blows through GitHub's limit
bd push  # Aborting push to github: provider rate limit hit (retry after 12m0s); 340 issue(s) skipped
// after: wait out the cooldown or raise the limit, then retry
bd push  # with higher-limit token, after cooldown
Defensive patterns

Strategy: retry

Validate before calling

// Before pushing, check remaining quota if the provider exposes it:
// e.g. GitHub: curl -s -H "Authorization: Bearer $TOKEN" https://api.github.com/rate_limit | jq .resources.core

Type guard

// Detect rate limiting the same way the engine does:
var rl tracker.RateLimitedError
if errors.As(err, &rl) {
    wait := rl.RateLimitRetryAfter()
    // schedule retry after `wait`
}

Try / catch

// Retry loop honoring the provider's retry-after:
for {
    err := engine.Push(ctx)
    if err == nil { break }
    var rl tracker.RateLimitedError
    if errors.As(err, &rl) {
        d := rl.RateLimitRetryAfter()
        if d <= 0 { d = time.Minute }
        time.Sleep(d)
        continue
    }
    return err
}

Prevention

When it happens

Trigger: `bd push` (doPush) to an external tracker (GitHub, GitLab, Jira, Linear, etc.) that responds with HTTP 429 / rate-limit errors; the engine detects it via errors.As(err, &RateLimitedError) and calls warnRateLimitAbort(err, remaining).

Common situations: Initial sync of a large backlog exceeding the provider's per-hour quota; CI jobs pushing on a shared token that has exhausted its rate limit; unauthenticated or low-tier tokens with small limits; bulk imports triggering secondary rate limits.

Related errors


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