{"record":{"id":"09f3ca2ea68e20da","repo":"affaan-m/ECC","slug":"request-retry-budget-exhausted","errorCode":null,"errorMessage":"request retry budget exhausted","messagePattern":"request retry budget exhausted","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/discord/release-announce.mjs","lineNumber":26,"sourceCode":"  discussionReceiptStatus,\n  findDiscussionReceipt,\n  findDiscordReceipt,\n  findReleaseDiscussion,\n  normalizeDiscordWebhookUrl,\n  releaseMarker,\n} from './announcement-core.mjs';\n\nconst env = process.env;\nconst sleep = ms => new Promise(resolve => setTimeout(resolve, ms));\n\nasync function request(url, options = {}, attempts = 3) {\n  for (let attempt = 1; attempt <= attempts; attempt += 1) {\n    const response = await fetch(url, options);\n    if (response.status !== 429 || attempt === attempts) return response;\n    const data = await response.json().catch(() => ({}));\n    await sleep(Math.min(Number(data.retry_after || 1) * 1000 + 250, 10_000));\n  }\n  throw new Error('request retry budget exhausted');\n}\n\nasync function githubGraphql(query, variables) {\n  const response = await request('https://api.github.com/graphql', {\n    method: 'POST',\n    headers: { Authorization: `Bearer ${env.GITHUB_TOKEN}`, 'Content-Type': 'application/json' },\n    body: JSON.stringify({ query, variables }),\n  });\n  if (!response.ok) throw new Error(`GitHub GraphQL request failed (${response.status})`);\n  const payload = await response.json();\n  if (payload.errors) throw new Error('GitHub GraphQL returned errors');\n  return payload.data;\n}\n\nasync function releaseFromGitHub() {\n  const [owner, repo] = env.GITHUB_REPOSITORY.split('/');\n  const tag = env.RELEASE_TAG || env.GITHUB_REF_NAME;\n  const response = await request(`https://api.github.com/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(tag)}`, {","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/discord/release-announce.mjs#L8-L44","documentation":"The request() helper retries fetch calls up to a fixed attempt budget when the API responds with HTTP 429, sleeping per the server's retry_after hint (capped at 10s). If every attempt returns 429 it throws 'request retry budget exhausted', indicating sustained rate limiting rather than a transient blip.","triggerScenarios":"Calling Discord webhook or GitHub endpoints faster than the per-route rate limit allows; many parallel workflow jobs announcing the same release; a webhook already near its 5/2s or 30/min limit; retry_after hints exceeding the 10s sleep cap so the backoff is insufficient.","commonSituations":"CI matrices fanning out announcements for the same tag; a looping bot script re-running every few seconds; a shared webhook used by multiple pipelines; GitHub secondary rate limits triggered by bursty API usage.","solutions":["Add exponential backoff with jitter and/or increase the attempt budget for 429-heavy routes.","Serialize or debounce announcement jobs so concurrent CI runs don't hammer the same webhook; cache the discussion lookup between runs.","Reduce call frequency: reuse the existing announcement discussion instead of re-querying on every invocation.","Inspect the 429 response body's retry_after and respect it fully instead of capping at 10s.","Spread webhook posts out — Discord enforces roughly 5 messages per 2 seconds per webhook."],"exampleFix":"// before\nconst response = await request(url, options);\n// after\nawait new Promise(r => setTimeout(r, 1000 + Math.random() * 2000)); // pre-request spacing\nconst response = await request(url, options);\nif (response.status === 429) {\n  const hint = (await response.json().catch(() => ({}))).retry_after ?? 5;\n  await new Promise(r => setTimeout(r, Math.min(hint * 1000, 30_000)));\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const res = await request(url, options);\n} catch (err) {\n  if (err.message === 'request retry budget exhausted') {\n    console.error('Still rate-limited after all retries; backing off before the next workflow run.');\n    process.exitCode = 1; // let CI retry later rather than hammering\n  } else {\n    throw err;\n  }\n}","preventionTips":["Honor Discord's ~5 messages / 2s per-webhook limit and GitHub secondary rate limits.","Add jittered exponential backoff rather than a fixed sleep cap.","Debounce concurrent CI jobs so only one announcement runs per release.","Reuse the existing announcement discussion instead of re-querying on every run."],"tags":["network","rate-limit","discord","github"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}