{"record":{"id":"7f814a359bac7803","repo":"anuraghazra/github-readme-stats","slug":"max-retry","errorCode":"MAX_RETRY","errorMessage":"Downtime due to GitHub API rate limiting","messagePattern":"Downtime due to GitHub API rate limiting","errorType":"error_code","errorClass":"CustomError","httpStatus":null,"severity":"error","filePath":"src/common/retryer.js","lineNumber":33,"sourceCode":" * @typedef {import(\"axios\").AxiosResponse} AxiosResponse Axios response.\n * @typedef {(variables: any, token: string, retriesForTests?: number) => Promise<AxiosResponse>} FetcherFunction Fetcher function.\n */\n\n/**\n * Try to execute the fetcher function until it succeeds or the max number of retries is reached.\n *\n * @param {FetcherFunction} fetcher The fetcher function.\n * @param {any} variables Object with arguments to pass to the fetcher function.\n * @param {number} retries How many times to retry.\n * @returns {Promise<any>} The response from the fetcher function.\n */\nconst retryer = async (fetcher, variables, retries = 0) => {\n  if (!RETRIES) {\n    throw new CustomError(\"No GitHub API tokens found\", CustomError.NO_TOKENS);\n  }\n\n  if (retries > RETRIES) {\n    throw new CustomError(\n      \"Downtime due to GitHub API rate limiting\",\n      CustomError.MAX_RETRY,\n    );\n  }\n\n  try {\n    // try to fetch with the first token since RETRIES is 0 index i'm adding +1\n    let response = await fetcher(\n      variables,\n      // @ts-ignore\n      process.env[`PAT_${retries + 1}`],\n      // used in tests for faking rate limit\n      retries,\n    );\n\n    // react on both type and message-based rate-limit signals.\n    // https://github.com/anuraghazra/github-readme-stats/issues/4425\n    const errors = response?.data?.errors;","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/anuraghazra/github-readme-stats/blob/54a7985aeefda00d5eadb55b80c17c7f976c37d2/src/common/retryer.js#L15-L51","documentation":"Thrown by the retryer once the recursion counter exceeds RETRIES (the number of available PAT_N tokens). The retryer increments 'retries' and recurses whenever GitHub signals RATE_LIMITED (by error type or a /rate limit/i message), returns 'Bad credentials', or returns 'account suspended'. When every token has been tried and exhausted, retries > RETRIES and this MAX_RETRY error fires. The secondary message tells users they can self-host or wait.","triggerScenarios":"All configured PAT_N tokens are rate-limited within one request: the retryer calls fetcher with PAT_1, sees RATE_LIMITED, recurses with PAT_2, and so on until retries > RETRIES. Also fires when every token returns 'Bad credentials' (revoked) or 'Sorry. Your account was suspended.', because those same branches recurse and burn the retry budget.","commonSituations":"Public Vercel instance under load where the shared token pool is exhausted; a token revoked by GitHub so each retry hits 'Bad credentials' and walks the whole list; suspended accounts; or a traffic spike against a self-hosted instance with too few tokens.","solutions":["Add more tokens (PAT_2, PAT_3, ...) so the retryer has more attempts before giving up.","Wait for the rate-limit window to reset (GitHub resets hourly) and retry.","Self-host the instance so you control token allocation, as the secondary message suggests.","Audit existing tokens for revocation/suspension — a single dead token is fine, but if all are dead every request walks the full list and fails."],"exampleFix":"// before: single exhausted/revoked token\n// PAT_1=ghp_revoked_or_rate_limited\n\n// after: pool of valid tokens\n// PAT_1=ghp_xxx\n// PAT_2=ghp_yyy\n// PAT_3=ghp_zzz","handlingStrategy":"retry","validationCode":"// Pre-flight: estimate remaining GitHub rate budget before issuing the request.\n// (Informational only — reduces, but cannot guarantee, MAX_RETRY.)\nasync function remainingRateBudget(token) {\n  const r = await fetch(\"https://api.github.com/rate_limit\", {\n    headers: { Authorization: `token ${token}` },\n  });\n  const j = await r.json();\n  return j.resources.graphql.remaining;\n}","typeGuard":null,"tryCatchPattern":"// Distinguish MAX_RETRY from other errors so you can back off rather than fail.\nimport { CustomError } from \"../src/common/error.js\";\ntry {\n  const stats = await fetchStats(username);\n} catch (err) {\n  if (err instanceof CustomError && err.type === CustomError.MAX_RETRY) {\n    // schedule a retry after the GitHub rate-limit window resets\n    await new Promise((r) => setTimeout(r, 60_000));\n    return fetchStats(username);\n  }\n  throw err;\n}","preventionTips":["Provision multiple tokens (PAT_1..PAT_N) so a single rate-limited token does not exhaust the retry budget.","Cache responses (the project sets CACHE_TTL) so repeated identical requests do not each consume token budget.","Self-host if you need predictable capacity rather than sharing the public instance's token pool."],"tags":["rate-limit","authentication","retry","github-api","transient"],"backgroundTag":null,"analyzedSha":"54a7985aeefda00d5eadb55b80c17c7f976c37d2","analyzedAt":"2026-08-12T23:20:41.776Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}