oven-sh/bun · error · Error

GitHub API rate limit exceeded after ${maxRetries} retries

Error message

GitHub API rate limit exceeded after ${maxRetries} retries

What it means

githubRequest() treats HTTP 429 and 403 as rate limiting and retries with backoff (retry-after header, else 1s * 2^n capped at 32s). After maxRetries attempts still receiving 429/403, it gives up. Important nuance: GitHub also answers 403 for permission problems, so an under-scoped token is misdiagnosed as rate limiting and will exhaust retries identically.

Source

Thrown at scripts/auto-close-duplicates.ts:76

  // Check rate limit headers
  const rateLimitRemaining = response.headers.get("x-ratelimit-remaining");
  const rateLimitReset = response.headers.get("x-ratelimit-reset");

  if (rateLimitRemaining && parseInt(rateLimitRemaining) < 100) {
    console.warn(`[WARNING] GitHub API rate limit low: ${rateLimitRemaining} requests remaining`);

    if (parseInt(rateLimitRemaining) < 10) {
      const resetTime = rateLimitReset ? parseInt(rateLimitReset) * 1000 : Date.now() + 60000;
      const waitTime = Math.max(0, resetTime - Date.now());
      console.warn(`[WARNING] Rate limit critically low, waiting ${Math.ceil(waitTime / 1000)}s until reset`);
      await sleep(waitTime + 1000); // Add 1s buffer
    }
  }

  // Handle rate limit errors with retry
  if (response.status === 429 || response.status === 403) {
    if (retryCount >= maxRetries) {
      throw new Error(`GitHub API rate limit exceeded after ${maxRetries} retries`);
    }

    const retryAfter = response.headers.get("retry-after");
    const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : Math.min(1000 * Math.pow(2, retryCount), 32000);

    console.warn(
      `[WARNING] Rate limited (${response.status}), retry ${retryCount + 1}/${maxRetries} after ${waitTime}ms`,
    );
    await sleep(waitTime);

    return githubRequest<T>(endpoint, token, method, body, retryCount + 1);
  }

  if (!response.ok) {
    throw new Error(`GitHub API request failed: ${response.status} ${response.statusText}`);
  }

  return response.json();

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. Use the Actions runtime GITHUB_TOKEN (1000+ req/h) instead of a personal token
  2. Honor the existing x-ratelimit-remaining < 10 pre-wait and add more spacing between page iterations
  3. If status is 403, check token scopes/permissions first — it may be an authorization failure, not a rate limit
  4. Wait for x-ratelimit-reset to pass and re-run the job

Example fix

// before
const token = process.env.MY_PAT; // 60 req/h

// after
const token = process.env.GITHUB_TOKEN; // Actions token, 1000+ req/h
// and distinguish permission failures from rate limits
if (response.status === 403 && !response.headers.get('x-ratelimit-remaining')) {
  throw new Error(`Forbidden (permissions?), not rate limited: ${await response.text()}`);
}
Defensive patterns

Strategy: retry

Validate before calling

null

Prevention

When it happens

Trigger: Paging all issues and comments of a large repo with a PAT (60 req/h) so every request 429s; secondary rate limits from a tight comment-paging loop; a token without repo scope returning 403 on each call until retries run out.

Common situations: Script run locally with a classic PAT instead of the Actions GITHUB_TOKEN; loop over hundreds of issues each fetching /comments pages; bursts that trip secondary limits.

Related errors


AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16). Data as JSON: /api/errors/2fe501be22d9da2e. Report an issue: GitHub.