jackwener/OpenCLI · warning · CommandExecutionError

${label} returned HTTP 429 (rate limited)

Error message

${label} returned HTTP 429 (rate limited)

What it means

The public formulae.brew.sh API rate-limits bursts of requests. When brewFetch receives HTTP 429 it throws a CommandExecutionError telling the caller the request was throttled and to wait a few seconds before retrying. The API is served as static files from GitHub Pages, so sustained rapid polling easily trips the limiter.

Source

Thrown at clis/homebrew/utils.js:75

    return s;
}

export async function brewFetch(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 ?? err}`,
            'Check that formulae.brew.sh is reachable from this network.',
        );
    }
    if (resp.status === 404) {
        throw new EmptyResultError(label, `Homebrew API returned 404 for ${url}.`);
    }
    if (resp.status === 429) {
        throw new CommandExecutionError(
            `${label} returned HTTP 429 (rate limited)`,
            'Homebrew throttles bursts; wait a few seconds 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 malformed JSON: ${err?.message ?? err}`);
    }
    return body;
}

/** Coerce a count value (which Homebrew analytics serves as `"139,972"`) to a plain number. */

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Wait a few seconds and retry the same request (the throttle is short-lived).
  2. Add delay/backoff between requests when iterating (e.g. sleep 1s per token, or exponential backoff on 429).
  3. Batch or cache results locally to cut request volume, and prefer sequential requests over Promise.all bursts.
  4. If you routinely need bulk data, download the API's bulk JSON files instead of per-token requests.

Example fix

// before
const results = await Promise.all(tokens.map(t => formula(t))); // 429 burst
// after
const results = [];
for (const t of tokens) {
  results.push(await withRetry(() => formula(t), { on429: waitMs => sleep(waitMs) }));
  await sleep(1000);
}
Defensive patterns

Strategy: retry

Validate before calling

const MIN_INTERVAL_MS = 1000;
let lastCall = 0;
async function throttledBrewFetch(url, label) {
  const wait = lastCall + MIN_INTERVAL_MS - Date.now();
  if (wait > 0) await sleep(wait);
  lastCall = Date.now();
  return brewFetch(url, label);
}

Type guard

null

Try / catch

async function fetchWith429Retry(url, label, retries = 3) {
  for (let i = 0; ; i++) {
    try { return await brewFetch(url, label); }
    catch (err) {
      if (err instanceof CommandExecutionError && err.message.includes('429') && i < retries) {
        await sleep(3000 * 2 ** i); // back off and retry
        continue;
      }
      throw err;
    }
  }
}

Prevention

When it happens

Trigger: Looping over many tokens without delay: for (const t of tokens) await formula(t); firing parallel requests with Promise.all over dozens of packages; a CI job re-running frequently against the same endpoints.

Common situations: Bulk scripts enumerating hundreds of formulae; monitoring/CI pipelines with short intervals; shared egress IPs (office/CI) where the combined request rate triggers 429 for everyone.

Understand the failure class

Related errors


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