{"record":{"id":"7e17f9cd68b0097d","repo":"jackwener/OpenCLI","slug":"label-rate-limited-http-429-back-off-and-ret","errorCode":null,"errorMessage":"${label} rate-limited (HTTP 429); back off and retry.","messagePattern":"(.+?) rate-limited \\(HTTP 429\\); back off and retry\\.","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":429,"severity":"warning","filePath":"clis/openfda/utils.js","lineNumber":38,"sourceCode":"    if (!Number.isInteger(n) || n < 1 || n > max) {\n        throw new ArgumentError(`--${name} must be an integer between 1 and ${max}`);\n    }\n    return n;\n}\n\nexport async function openfdaFetch(url, label) {\n    let resp;\n    try {\n        resp = await fetch(url, { headers: { 'User-Agent': UA, accept: 'application/json' } });\n    } catch (err) {\n        throw new CommandExecutionError(`${label} request failed: ${err.message}`);\n    }\n    if (resp.status === 404) {\n        // openFDA returns 404 for \"no matches\" instead of an empty results array.\n        throw new EmptyResultError(label, `${label} returned 404 (no matches).`);\n    }\n    if (resp.status === 429) {\n        throw new CommandExecutionError(`${label} rate-limited (HTTP 429); back off and retry.`);\n    }\n    if (!resp.ok) {\n        throw new CommandExecutionError(`${label} returned HTTP ${resp.status}.`);\n    }\n    let body;\n    try {\n        body = await resp.json();\n    } catch (err) {\n        throw new CommandExecutionError(`${label} returned non-JSON body: ${err.message}`);\n    }\n    return body;\n}\n\n// openFDA returns most string fields as `[string]` arrays — collapse to first\n// element. Preserves `null` (not coerced to empty string) when the slot is\n// missing entirely.\nexport function firstOrNull(arr) {\n    if (!Array.isArray(arr) || !arr.length) return null;","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/openfda/utils.js#L20-L56","documentation":"openFDA enforces rate limits (240 requests/minute without an API key); when exceeded it returns HTTP 429 and openfdaFetch converts that to this CommandExecutionError. It tells the caller to back off rather than hammering the API. The label identifies which command's request was throttled.","triggerScenarios":"A script loops over many drug/recall queries faster than 240/min without an api.fda.gov API key; multiple concurrent workers share one IP; a burst of retries after earlier failures compounds the throttling.","commonSituations":"Batch job enumerating hundreds of drug names overnight; CI pipeline running the CLI in a tight loop; shared office/NAT IP where combined traffic crosses the limit; missing OPENFDA_API_KEY configured into the request.","solutions":["Add a delay between requests (e.g. 300-500ms) or use exponential backoff when 429 appears.","Register for a free openFDA API key and include it, raising the allowance.","Reduce concurrency — serialize requests instead of parallel fan-out.","Cache responses for repeated queries to cut request volume."],"exampleFix":"// before\nfor (const drug of drugs) await fetchDrugLabel({ generic: drug }); // bursts -> 429\n// after\nfor (const drug of drugs) {\n  await fetchDrugLabel({ generic: drug });\n  await new Promise(r => setTimeout(r, 500)); // throttle to stay under limit\n}","handlingStrategy":"retry","validationCode":"class RateLimiter {\n  constructor(minIntervalMs = 300) { this.min = minIntervalMs; this.last = 0; }\n  async wait() {\n    const now = Date.now();\n    const delta = now + this.min - this.last;\n    if (delta > 0) await new Promise(r => setTimeout(r, delta));\n    this.last = Date.now();\n  }\n}","typeGuard":"function isRateLimitError(e) {\n  return e instanceof Error && /429/.test(e.message);\n}","tryCatchPattern":"try {\n  const body = await openfdaFetch(url, label);\n} catch (e) {\n  if (/rate-limited \\(HTTP 429\\)/.test(e.message)) {\n    await new Promise(r => setTimeout(r, 30_000)); // long backoff\n    return openfdaFetch(url, label);\n  }\n  throw e;\n}","preventionTips":["Throttle requests to under 240/min (delay ~300ms between calls).","Get a free openFDA API key to raise the limit.","Serialize batch jobs instead of parallel fan-out.","Cache repeated queries to reduce request volume."],"tags":["rate-limit","http-429","openfda","retry"],"backgroundTag":"rate-limited-429","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}