{"record":{"id":"16e9d68f9a515ae9","repo":"jackwener/OpenCLI","slug":"label-returned-http-429-rate-limited-lichess","errorCode":null,"errorMessage":"${label} returned HTTP 429 (rate limited). Lichess throttles anonymous traffic at ~60 req/min; back off and retry.","messagePattern":"(.+?) returned HTTP 429 \\(rate limited\\)\\. Lichess throttles anonymous traffic at ~60 req/min; back off and retry\\.","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":429,"severity":"warning","filePath":"clis/lichess/utils.js","lineNumber":73,"sourceCode":"    return n;\n}\n\nexport async function lichessFetch(url, label) {\n    let resp;\n    try {\n        resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });\n    }\n    catch (err) {\n        throw new CommandExecutionError(\n            `${label} request failed: ${err?.message ?? err}`,\n            'Check that lichess.org is reachable from this network.',\n        );\n    }\n    if (resp.status === 404) {\n        throw new EmptyResultError(label, `Lichess returned 404 for ${url}.`);\n    }\n    if (resp.status === 429) {\n        throw new CommandExecutionError(\n            `${label} returned HTTP 429 (rate limited)`,\n            'Lichess throttles anonymous traffic at ~60 req/min; back off and retry.',\n        );\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    }\n    catch (err) {\n        throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);\n    }\n    return body;\n}\n\n/** Format a lichess unix-ms timestamp as ISO date (YYYY-MM-DD). `null` when missing. */","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/lichess/utils.js#L55-L91","documentation":"This CommandExecutionError is thrown by `lichessFetch` when the Lichess API responds with HTTP 429 (Too Many Requests). Lichess rate-limits anonymous traffic (~60 req/min); the library surfaces this with advice to back off and retry.","triggerScenarios":"Any command routed through `lichessFetch` after exceeding Lichess's rate limit — e.g. looping over many usernames without delay, or sharing an IP (CI runner, office NAT) that already hit the anonymous cap.","commonSituations":"Batch scripts iterating many players with no sleep; retry loops without backoff amplifying the limit; shared CI egress IPs; multiple tools hammering the API concurrently.","solutions":["Add delay between requests and retry with exponential backoff honoring Retry-After","Cache responses to avoid re-fetching the same data","Authenticate with an OAuth token if you need higher limits, and still throttle","Reduce request volume: batch or filter the entities you query","Retry later if on a shared IP — the limit is per-IP for anonymous traffic"],"exampleFix":"// before\nfor (const n of names) await user(n); // bursts past 60 req/min\n// after\nfor (const n of names) {\n  await user(n);\n  await new Promise(r => setTimeout(r, 1100)); // stay under ~60 req/min\n}","handlingStrategy":"retry","validationCode":"// Self-throttle before each call to stay under ~60 req/min\nclass Limiter {\n  constructor(ms = 1100) { this.ms = ms; }\n  async wait() { await new Promise(r => setTimeout(r, this.ms)); }\n}\nconst limiter = new Limiter();\nawait limiter.wait();\nawait user(name);","typeGuard":null,"tryCatchPattern":"async function withBackoff(fn, retries = 4) {\n  for (let i = 0; ; i++) {\n    try { return await fn(); }\n    catch (e) {\n      const is429 = e instanceof CommandExecutionError && /429/.test(e.message);\n      if (!is429 || i >= retries) throw e;\n      await new Promise(r => setTimeout(r, 2 ** i * 1000));\n    }\n  }\n}\nconst profile = await withBackoff(() => user(name));","preventionTips":["Space requests ~1s apart to stay under the ~60 req/min anonymous cap","Never retry 429s in a tight loop — use exponential backoff","Cache API responses to avoid redundant lookups","Add jitter when running many parallel workers on one IP","Note shared CI/office egress IPs count toward the same limit"],"tags":["rate-limit","http-429","lichess","backoff"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}