{"record":{"id":"d335ba6266fc8a1f","repo":"amruthpillai/reactive-resume","slug":"rate-limit-exceeded","errorCode":"RATE_LIMIT_EXCEEDED","errorMessage":"Public resume rendering rate limit exceeded.","messagePattern":"Public resume rendering rate limit exceeded\\.","errorType":"exception","errorClass":"ORPCError","httpStatus":429,"severity":"warning","filePath":"packages/api/src/features/resume/public-render-rate-limit.ts","lineNumber":41,"sourceCode":"\tconst capacity = options.capacity ?? 6;\n\tconst refillWindowMs = options.refillWindowMs ?? 60_000;\n\tconst now = options.now ?? Date.now;\n\tif (!Number.isInteger(capacity) || capacity <= 0 || !Number.isFinite(refillWindowMs) || refillWindowMs <= 0) {\n\t\tthrow new Error(\"Public render token bucket requires positive finite limits\");\n\t}\n\tconst buckets = new Map<string, Bucket>();\n\n\treturn {\n\t\tconsume(input) {\n\t\t\tconst currentTime = now();\n\t\t\tconst trustedClient = input.trustedClient.trim() || \"unknown\";\n\t\t\tconst key = `${trustedClient}:${input.resumeId}`;\n\t\t\tconst previous = buckets.get(key) ?? { tokens: capacity, updatedAt: currentTime };\n\t\t\tconst elapsed = Math.max(0, currentTime - previous.updatedAt);\n\t\t\tconst tokens = Math.min(capacity, previous.tokens + (elapsed * capacity) / refillWindowMs);\n\n\t\t\tif (tokens < 1) {\n\t\t\t\tthrow new ORPCError(\"RATE_LIMIT_EXCEEDED\", {\n\t\t\t\t\tstatus: 429,\n\t\t\t\t\tmessage: \"Public resume rendering rate limit exceeded.\",\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tbuckets.delete(key);\n\t\t\tbuckets.set(key, { tokens: tokens - 1, updatedAt: currentTime });\n\t\t\tif (buckets.size <= MAX_BUCKETS) return;\n\n\t\t\tfor (const [candidate, bucket] of buckets) {\n\t\t\t\tif (currentTime - bucket.updatedAt >= refillWindowMs) buckets.delete(candidate);\n\t\t\t}\n\t\t\tif (buckets.size > MAX_BUCKETS) buckets.delete(buckets.keys().next().value as string);\n\t\t},\n\t};\n}\n\nexport const publicRenderRateLimiter = createPublicRenderRateLimiter();","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/amruthpillai/reactive-resume/blob/3a5b12e2a40374a9571988701fcb75c5a1831c42/packages/api/src/features/resume/public-render-rate-limit.ts#L23-L59","documentation":"A per-(trustedClient,resumeId) token bucket (default capacity 6, full refill over 60s) guards public resume rendering. consume() throws RATE_LIMIT_EXCEEDED (HTTP 429) when fewer than 1 token is available. The limiter is in-memory and process-local, so each server process has its own bucket.","triggerScenarios":"More than 6 public PDF renders of the same resume from the same trustedClient identifier within ~60 seconds, or sustained rendering faster than the refill rate (1 token per ~10s).","commonSituations":"A page with aggressive auto-refresh, a bot crawling public resumes, multiple users behind a shared client identifier, or a load test hammering the fallback endpoint.","solutions":["Throttle client-side: cache the rendered PDF and avoid re-requesting within the window.","If legitimately raising the limit, adjust capacity/refillWindowMs via createPublicRenderRateLimiter options (server-side).","Prefer the client-side projection path so the server fallback render is never hit."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"const RENDER_BUDGET = 6; // capacity per 60s window per (client,resume)\nfunction canRender(state, resumeId) {\n  const now = Date.now();\n  const used = state.counts.get(`${state.client}:${resumeId}`) ?? { n: 0, t: now };\n  const elapsed = now - used.t;\n  const tokens = Math.min(RENDER_BUDGET, used.n + (elapsed * RENDER_BUDGET) / 60000);\n  return tokens >= 1;\n}","typeGuard":null,"tryCatchPattern":"async function renderWithBackoff(fn) {\n  for (let attempt = 0; attempt < 3; attempt++) {\n    try { return await fn(); }\n    catch (e) {\n      if (e.code !== 'RATE_LIMIT_EXCEEDED') throw e;\n      await new Promise(r => setTimeout(r, (attempt + 1) * 10000)); // ~1 token / 10s\n    }\n  }\n  throw new Error('Public render rate limit exhausted');\n}","preventionTips":["Cache rendered public PDFs client-side and avoid re-rendering within the window.","Prefer the client-side projection path over the server fallback render.","Throttle automated/crawling traffic against public resume endpoints."],"tags":["resume","public-pdf","rate-limit","semantic-css"],"backgroundTag":null,"analyzedSha":"3a5b12e2a40374a9571988701fcb75c5a1831c42","analyzedAt":"2026-08-12T22:31:22.666Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}