{"record":{"id":"e3fe11489299e7aa","repo":"screenpipe/screenpipe","slug":"hosted-ai-admission-rejected","errorCode":null,"errorMessage":"hosted AI admission rejected","messagePattern":"hosted AI admission rejected","errorType":"http","errorClass":null,"httpStatus":429,"severity":"warning","filePath":"packages/ai-gateway/src/index.ts","lineNumber":634,"sourceCode":"\t\t\t);\n\t\t\tif (gate === 'downgrade') {\n\t\t\t\tconsole.log(`background request for disallowed model \"${body.model}\" (${authResult.tier}) -> downgraded to auto`);\n\t\t\t\tbody.model = 'auto';\n\t\t\t} else if (gate === 'reject') {\n\t\t\t\treturn modelNotAllowedResponse(authResult, body.model);\n\t\t\t}\n\n\t\t\t// Per-minute rate limit. Now that the model is resolved (a 'downgrade'\n\t\t\t// already rewrote it to free 'auto'), free weight-0 models meter\n\t\t\t// against the high `freeRpm` bucket — so \"switch to a free model to\n\t\t\t// avoid rate limits\" actually works. Paid models keep the low `rpm`.\n\t\t\t// The two buckets are independent; the daily cost cap below is the\n\t\t\t// real backstop against runaway free loops.\n\t\t\tconst rateLimit = await checkRateLimit(request, env, authResult, {\n\t\t\t\tfreeModel: isFreeModel(body.model),\n\t\t\t});\n\t\t\tif (!rateLimit.allowed && rateLimit.response) {\n\t\t\t\tconsole.warn('hosted AI admission rejected', {\n\t\t\t\t\tgate: 'per_minute',\n\t\t\t\t\ttier: authResult.tier,\n\t\t\t\t\taccountPlan: authResult.accountPlan,\n\t\t\t\t});\n\t\t\t\treturn rateLimit.response;\n\t\t\t}\n\n\t\t\tconst cloudflareGateway = isHostedChatGatewayEnabled(env);\n\t\t\tlet legacyRescueFallback = false;\n\t\t\t// Legacy mode retains the paid weighted-query admission gate. In\n\t\t\t// Cloudflare mode the provider-cost spend rules are authoritative for\n\t\t\t// this endpoint; Free's separate two-message lease remains above.\n\t\t\tlet usage: Awaited<ReturnType<typeof trackUsage>> | null = null;\n\t\t\tif (!cloudflareGateway) {\n\t\t\t\tconst ipAddress = request.headers.get('cf-connecting-ip') || undefined;\n\t\t\t\tusage = await trackUsage(env, authResult.deviceId, usageTier, authResult.userId, ipAddress, body.model);\n\t\t\t}\n\t\t\tif (usage && !usage.allowed) {","sourceCodeStart":616,"sourceCodeEnd":652,"githubUrl":"https://github.com/screenpipe/screenpipe/blob/4ebf712990fee17eeaf904dacf749b6e96ac9bf3/packages/ai-gateway/src/index.ts#L616-L652","documentation":"Server-side log line emitted by the screenpipe AI gateway worker (`handleRequest`, the worker `fetch` handler) when a request is rejected by the per-minute rate limiter (`checkRateLimit`). It is not thrown at the caller — the caller receives the limiter's 429-style response — but the log records the admission decision with the auth tier and account plan. Paid and free models meter against independent buckets (free weight-0 models use a high freeRpm bucket, paid models a low rpm bucket).","triggerScenarios":"A client authenticated as a particular tier (anonymous/free/paid) sends chat completion requests to the gateway faster than its tier's per-minute allowance: e.g. an interactive chat client hammering the endpoint, an automation loop polling, or Pi tool-loop calls without session affinity exceeding the free per-account two-message lease's RPM side.","commonSituations":"Runaway retry loops in user code that immediately retry on errors without backoff; shared egress IPs (NAT, VPN) aggregating many anonymous devices against one key; burst tool-calling agents issuing many parallel completions; switching to a paid model after exhausting the free bucket (independent buckets mean both can trip).","solutions":["Inspect the 429 response body/headers returned with the log — it carries reset/retry info; pause and retry after the window resets instead of immediately re-sending.","Add exponential backoff with jitter to your client's retry logic, and serialize requests instead of firing them in parallel.","If on the free tier, switch the request's `model` to a free weight-0 model (e.g. 'auto') — free models use the separate high freeRpm bucket.","If you legitimately need higher RPM, upgrade the account plan/tier so checkRateLimit uses the paid bucket."],"exampleFix":"// before: tight retry loop\nfor (;;) { await fetch(GATEWAY, { method: 'POST', body }); }\n// after: respect backoff on 429\nfor (;;) {\n  const res = await fetch(GATEWAY, { method: 'POST', body });\n  if (res.status !== 429) break;\n  const retryAfter = Number(res.headers.get('retry-after') ?? 30);\n  await new Promise(r => setTimeout(r, retryAfter * 1000));\n}","handlingStrategy":"retry","validationCode":"// client-side: throttle before calling\nlet lastCall = 0;\nasync function throttledCall(fn, minIntervalMs = 3000) {\n  const wait = lastCall + minIntervalMs - Date.now();\n  if (wait > 0) await new Promise(r => setTimeout(r, wait));\n  lastCall = Date.now();\n  return fn();\n}","typeGuard":null,"tryCatchPattern":"const res = await fetch(GATEWAY, opts);\nif (res.status === 429) {\n  const retryAfter = Number(res.headers.get('retry-after') ?? 30);\n  await sleep(retryAfter * 1000 * (1 + Math.random() * 0.5)); // jittered backoff\n  return retry();\n}","preventionTips":["Implement exponential backoff with jitter on every 429 from the gateway.","Serialize chat calls instead of issuing parallel completions.","On the free tier, use free weight-0 models (e.g. 'auto') to hit the high freeRpm bucket.","Cap automation/tool-loop concurrency and add session-affinity headers where supported.","Upgrade the account tier if sustained RPM above the free bucket is needed."],"tags":["rate-limit","http-429","cloudflare-workers","ai-gateway"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"4ebf712990fee17eeaf904dacf749b6e96ac9bf3","analyzedAt":"2026-09-01T23:33:43.065Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T06:17:21.866Z"}