jackwener/OpenCLI · warning · CommandExecutionError

${label} rate-limited (HTTP 429); back off and retry.

Error message

${label} rate-limited (HTTP 429); back off and retry.

What it means

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.

Source

Thrown at clis/openfda/utils.js:38

    if (!Number.isInteger(n) || n < 1 || n > max) {
        throw new ArgumentError(`--${name} must be an integer between 1 and ${max}`);
    }
    return n;
}

export async function openfdaFetch(url, label) {
    let resp;
    try {
        resp = await fetch(url, { headers: { 'User-Agent': UA, accept: 'application/json' } });
    } catch (err) {
        throw new CommandExecutionError(`${label} request failed: ${err.message}`);
    }
    if (resp.status === 404) {
        // openFDA returns 404 for "no matches" instead of an empty results array.
        throw new EmptyResultError(label, `${label} returned 404 (no matches).`);
    }
    if (resp.status === 429) {
        throw new CommandExecutionError(`${label} rate-limited (HTTP 429); back off and retry.`);
    }
    if (!resp.ok) {
        throw new CommandExecutionError(`${label} returned HTTP ${resp.status}.`);
    }
    let body;
    try {
        body = await resp.json();
    } catch (err) {
        throw new CommandExecutionError(`${label} returned non-JSON body: ${err.message}`);
    }
    return body;
}

// openFDA returns most string fields as `[string]` arrays — collapse to first
// element. Preserves `null` (not coerced to empty string) when the slot is
// missing entirely.
export function firstOrNull(arr) {
    if (!Array.isArray(arr) || !arr.length) return null;

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Add a delay between requests (e.g. 300-500ms) or use exponential backoff when 429 appears.
  2. Register for a free openFDA API key and include it, raising the allowance.
  3. Reduce concurrency — serialize requests instead of parallel fan-out.
  4. Cache responses for repeated queries to cut request volume.

Example fix

// before
for (const drug of drugs) await fetchDrugLabel({ generic: drug }); // bursts -> 429
// after
for (const drug of drugs) {
  await fetchDrugLabel({ generic: drug });
  await new Promise(r => setTimeout(r, 500)); // throttle to stay under limit
}
Defensive patterns

Strategy: retry

Validate before calling

class RateLimiter {
  constructor(minIntervalMs = 300) { this.min = minIntervalMs; this.last = 0; }
  async wait() {
    const now = Date.now();
    const delta = now + this.min - this.last;
    if (delta > 0) await new Promise(r => setTimeout(r, delta));
    this.last = Date.now();
  }
}

Type guard

function isRateLimitError(e) {
  return e instanceof Error && /429/.test(e.message);
}

Try / catch

try {
  const body = await openfdaFetch(url, label);
} catch (e) {
  if (/rate-limited \(HTTP 429\)/.test(e.message)) {
    await new Promise(r => setTimeout(r, 30_000)); // long backoff
    return openfdaFetch(url, label);
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/7e17f9cd68b0097d. Report an issue: GitHub.