jackwener/OpenCLI · warning · CommandExecutionError

${label} returned HTTP 429 (rate limited)

Error message

${label} returned HTTP 429 (rate limited)

What it means

openalexFetch detects HTTP 429 from api.openalex.org and throws CommandExecutionError indicating the request was rate limited, with a hint to wait and retry or set OPENALEX_MAILTO. OpenAlex throttles unauthenticated traffic to its common pool; providing a mailto= parameter moves requests into the faster 'polite pool'.

Source

Thrown at clis/openalex/utils.js:96

    );
}

export async function openalexFetch(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 api.openalex.org is reachable from this network.',
        );
    }
    if (resp.status === 404) {
        throw new EmptyResultError(label, `OpenAlex returned 404 for ${url}.`);
    }
    if (resp.status === 429) {
        throw new CommandExecutionError(
            `${label} returned HTTP 429 (rate limited)`,
            'OpenAlex throttles unauthenticated traffic; wait a few seconds and retry, or set OPENALEX_MAILTO.',
        );
    }
    if (!resp.ok) {
        let detail = '';
        try {
            const text = await resp.text();
            const match = text.match(/"message"\s*:\s*"([^"]+)"/);
            if (match) detail = ` (${match[1]})`;
        }
        catch { /* ignore */ }
        throw new CommandExecutionError(`${label} returned HTTP ${resp.status}${detail}`);
    }
    let body;
    try {
        body = await resp.json();
    }

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Set OPENALEX_MAILTO (or append ?mailto=you@example.com) to join the polite pool
  2. Add throttling: sleep ~150-200ms between requests, or use a concurrency limit of 1-5
  3. Back off and retry on 429 honoring any Retry-After header
  4. Batch queries where possible (e.g. filter=ids:W1|W2|… in one request) to cut request count

Example fix

// before
for (const id of ids) await work(id); // bursts -> 429
// after
await Promise.all(ids.map(id =>
  withBackoff(() => work(id))
));
async function withBackoff(fn, tries = 5) {
  try { return await fn(); }
  catch (e) {
    if (tries && String(e).includes('429')) {
      await new Promise(r => setTimeout(r, 2 ** (5 - tries) * 500));
      return withBackoff(fn, tries - 1);
    }
    throw e;
  }
}
Defensive patterns

Strategy: retry

Validate before calling

// ensure mailto is configured before bulk runs
if (!process.env.OPENALEX_MAILTO) {
  console.warn('Set OPENALEX_MAILTO to join the OpenAlex polite pool and avoid 429s');
}

Try / catch

async function fetchWithBackoff(fn, maxTries = 5) {
  for (let i = 0; i < maxTries; i++) {
    try { return await fn(); }
    catch (e) {
      const is429 = e.name === 'CommandExecutionError' && e.message.includes('429');
      if (!is429 || i === maxTries - 1) throw e;
      await new Promise(r => setTimeout(r, 2 ** i * 500));
    }
  }
}

Prevention

When it happens

Trigger: Issuing many openalex requests in a short window without mailto, exceeding OpenAlex's unauthenticated rate limit (roughly 10 req/s), causing the API to respond 429.

Common situations: Bulk scripts looping over hundreds of IDs with no delay; CI jobs hammering the API in parallel; shared-IP environments (offices, CI runners) exhausting the collective unauthenticated quota.

Understand the failure class

Related errors


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