{"record":{"id":"ed0b8a4af62a5e06","repo":"heygen-com/hyperframes","slug":"rate-limited","errorCode":"RATE_LIMITED","errorMessage":"figma rate limit hit (429) and still limited after ${maxRetries} retries — wait a minute and re-run, or import fewer nodes per call.","messagePattern":"figma rate limit hit \\(429\\) and still limited after (.+?) retries — wait a minute and re-run, or import fewer nodes per call\\.","errorType":"error_code","errorClass":"FigmaClientError","httpStatus":429,"severity":"warning","filePath":"packages/core/src/figma/client.ts","lineNumber":281,"sourceCode":"      `figma denied access (403). ${scopeLine} Also confirm the file is visible to your account.`,\n      403,\n      opts.endpoint,\n    );\n  }\n\n  /** Throw the typed error for a non-ok response (no-op when res.ok). */\n  async function throwForStatus(res: Response, path: string, opts: GetOptions): Promise<void> {\n    if (res.ok) return;\n    if (res.status === 401)\n      throw new FigmaClientError(\n        \"BAD_TOKEN\",\n        \"figma rejected the token (401) — it is expired or revoked. Re-mint at figma.com/settings → Security, then update FIGMA_TOKEN.\",\n        401,\n        opts.endpoint,\n      );\n    if (res.status === 403) throw forbiddenError(await readFigmaErrorMessage(res), opts);\n    if (res.status === 429)\n      throw new FigmaClientError(\n        \"RATE_LIMITED\",\n        `figma rate limit hit (429) and still limited after ${maxRetries} retries — wait a minute and re-run, or import fewer nodes per call.`,\n        429,\n        opts.endpoint,\n      );\n    throw new FigmaClientError(\n      \"HTTP_ERROR\",\n      `figma request failed: HTTP ${res.status} ${path}`,\n      res.status,\n      opts.endpoint,\n    );\n  }\n\n  async function get(path: string, opts: GetOptions): Promise<unknown> {\n    // Retry 429 with backoff before surfacing RATE_LIMITED — figma's limit is\n    // per-minute, so a couple of imports in quick succession hit it and a\n    // short wait clears it. Honor Retry-After when present, else exponential.\n    let res: Response;","sourceCodeStart":263,"sourceCodeEnd":299,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/core/src/figma/client.ts#L263-L299","documentation":"Thrown by throwForStatus (code RATE_LIMITED, status 429) only AFTER the get() retry loop has already attempted up to maxRetries (default 3) requests with backoff — honoring Retry-After when figma sends it, else exponential 1s/2s/4s. Reaching this throw means the per-minute rate budget was still exhausted after those waits. The client deliberately surfaces it rather than blocking silently because, past a few retries, the user is better off reducing batch size or waiting a minute than watching the CLI hang.","triggerScenarios":"Issuing many renderNodes/nodeTree calls in a tight loop within the same minute; a single batched renderNodes call with a very large nodeIds list; figma tier-level quota exhaustion (Retry-After in the thousands of seconds, capped at MAX_RETRY_WAIT_MS=60s by retryAfterMs); running multiple CLI processes against the same token concurrently.","commonSituations":"Bulk-importing dozens of figma frames in one script run; CI matrix jobs sharing a token; a long export session that creeps over the per-minute limit near the end; the free figma tier's lower rate ceiling.","solutions":["Wait ~60 seconds and re-run — figma's limit is per-minute and a single wait usually clears it.","Reduce the number of nodes per renderNodes call, or space successive calls with a small delay.","Batch node renders into a single renderNodes call (comma-separated ids) rather than many renderNode calls — this is figma's own recommended workaround.","If it happens routinely, raise maxRetries or pass a custom sleep to spread retries further."],"exampleFix":"// before — many single-node calls hammer the per-minute limit\nfor (const id of nodeIds) {\n  await client.renderNode({ fileKey, nodeId: id }, { format: 'png' });\n}\n\n// after — one batched call, figma's documented rate-limit workaround\nawait client.renderNodes(fileKey, nodeIds, { format: 'png' });","handlingStrategy":"retry","validationCode":null,"typeGuard":"import { FigmaClientError } from '.../figma/client';\nexport function isRateLimited(err: unknown): err is FigmaClientError {\n  return err instanceof FigmaClientError && err.code === 'RATE_LIMITED';\n}","tryCatchPattern":"async function withRateLimitRetry<T>(fn: () => Promise<T>): Promise<T> {\n  for (let attempt = 0; ; attempt++) {\n    try { return await fn(); }\n    catch (err) {\n      if (isRateLimited(err) && attempt < 2) {\n        await new Promise(r => setTimeout(r, 60_000)); // figma limit is per-minute\n        continue;\n      }\n      throw err;\n    }\n  }\n}","preventionTips":["Batch node renders into a single renderNodes call (figma's documented workaround).","Throttle concurrent figma calls with a per-minute token bucket in your importer.","Tune maxRetries and the injected sleep in tests (no-op sleep) so unit tests don't block on retry."],"tags":["figma","rate-limit","retry","network","transient"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}