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
- Wait for the cooldown shown in the message (RateLimitRetryAfter), then re-run the push; the remaining issues will be retried.
- Use a token with a higher rate limit or dedicated to this automation (avoid shared CI tokens).
- Reduce batch size / throttle pushes (push in smaller batches spaced over time).
- Ensure the provider client is authenticating — anonymous limits are far lower.
- 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
- Use a dedicated, authenticated token with sufficient rate limits for automation.
- Batch or throttle pushes; avoid full-backlog pushes on fresh installations during peak windows.
- Schedule syncs spaced out in CI rather than on every commit.
- Respect the reported cooldown — immediate retries usually extend the limit.
- Monitor provider rate-limit headers/quota dashboards in automation.
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
- transient error %d (attempt %d/%d)
- transient error %d (attempt %d/%d)
- non-retryable error: %w
- failed after %d retries: %w
- transient error %d (attempt %d/%d)
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/2a9a930a37b4792d.
Report an issue: GitHub.