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
- Use the Actions runtime GITHUB_TOKEN (1000+ req/h) instead of a personal token
- Honor the existing x-ratelimit-remaining < 10 pre-wait and add more spacing between page iterations
- If status is 403, check token scopes/permissions first — it may be an authorization failure, not a rate limit
- 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
- Always use the Actions GITHUB_TOKEN (or an app installation token) instead of a PAT for bulk paging
- Read x-ratelimit-remaining before each page and pause near zero (the script already waits under 10)
- Distinguish 403-without-ratelimit-headers (permissions) from real rate limits before retrying
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
- github ${path}: ${res.status}
- Failed to fetch Sentry event: ${response.statusText}
- Failed to fetch Sentry issue: ${issueResponse.statusText}
- GitHub API request failed: ${response.status} ${response.sta
- GITHUB_TOKEN environment variable is required
AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16).
Data as JSON: /api/errors/2fe501be22d9da2e.
Report an issue: GitHub.