amruthpillai/reactive-resume · warning · ORPCError
RATE_LIMIT_EXCEEDED
RATE_LIMIT_EXCEEDED
Error message
Public resume rendering rate limit exceeded.
What it means
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.
Source
Thrown at packages/api/src/features/resume/public-render-rate-limit.ts:41
const capacity = options.capacity ?? 6;
const refillWindowMs = options.refillWindowMs ?? 60_000;
const now = options.now ?? Date.now;
if (!Number.isInteger(capacity) || capacity <= 0 || !Number.isFinite(refillWindowMs) || refillWindowMs <= 0) {
throw new Error("Public render token bucket requires positive finite limits");
}
const buckets = new Map<string, Bucket>();
return {
consume(input) {
const currentTime = now();
const trustedClient = input.trustedClient.trim() || "unknown";
const key = `${trustedClient}:${input.resumeId}`;
const previous = buckets.get(key) ?? { tokens: capacity, updatedAt: currentTime };
const elapsed = Math.max(0, currentTime - previous.updatedAt);
const tokens = Math.min(capacity, previous.tokens + (elapsed * capacity) / refillWindowMs);
if (tokens < 1) {
throw new ORPCError("RATE_LIMIT_EXCEEDED", {
status: 429,
message: "Public resume rendering rate limit exceeded.",
});
}
buckets.delete(key);
buckets.set(key, { tokens: tokens - 1, updatedAt: currentTime });
if (buckets.size <= MAX_BUCKETS) return;
for (const [candidate, bucket] of buckets) {
if (currentTime - bucket.updatedAt >= refillWindowMs) buckets.delete(candidate);
}
if (buckets.size > MAX_BUCKETS) buckets.delete(buckets.keys().next().value as string);
},
};
}
export const publicRenderRateLimiter = createPublicRenderRateLimiter();View on GitHub (pinned to 3a5b12e2a4)
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.
Defensive patterns
Strategy: retry
Validate before calling
const RENDER_BUDGET = 6; // capacity per 60s window per (client,resume)
function canRender(state, resumeId) {
const now = Date.now();
const used = state.counts.get(`${state.client}:${resumeId}`) ?? { n: 0, t: now };
const elapsed = now - used.t;
const tokens = Math.min(RENDER_BUDGET, used.n + (elapsed * RENDER_BUDGET) / 60000);
return tokens >= 1;
} Try / catch
async function renderWithBackoff(fn) {
for (let attempt = 0; attempt < 3; attempt++) {
try { return await fn(); }
catch (e) {
if (e.code !== 'RATE_LIMIT_EXCEEDED') throw e;
await new Promise(r => setTimeout(r, (attempt + 1) * 10000)); // ~1 token / 10s
}
}
throw new Error('Public render rate limit exhausted');
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- BAD_REQUEST
- STYLESHEET_PARITY_FAILED
- STYLESHEET_REVISION_CONFLICT
- An unknown error occurred while validating the merged resume
- NOT_FOUND
AI-assisted analysis of amruthpillai/reactive-resume@3a5b12e2a4 (2026-08-12).
Data as JSON: /api/errors/d335ba6266fc8a1f.
Report an issue: GitHub.