screenpipe/screenpipe · warning
hosted AI admission rejected
Error message
hosted AI admission rejected
What it means
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).
Source
Thrown at packages/ai-gateway/src/index.ts:634
);
if (gate === 'downgrade') {
console.log(`background request for disallowed model "${body.model}" (${authResult.tier}) -> downgraded to auto`);
body.model = 'auto';
} else if (gate === 'reject') {
return modelNotAllowedResponse(authResult, body.model);
}
// Per-minute rate limit. Now that the model is resolved (a 'downgrade'
// already rewrote it to free 'auto'), free weight-0 models meter
// against the high `freeRpm` bucket — so "switch to a free model to
// avoid rate limits" actually works. Paid models keep the low `rpm`.
// The two buckets are independent; the daily cost cap below is the
// real backstop against runaway free loops.
const rateLimit = await checkRateLimit(request, env, authResult, {
freeModel: isFreeModel(body.model),
});
if (!rateLimit.allowed && rateLimit.response) {
console.warn('hosted AI admission rejected', {
gate: 'per_minute',
tier: authResult.tier,
accountPlan: authResult.accountPlan,
});
return rateLimit.response;
}
const cloudflareGateway = isHostedChatGatewayEnabled(env);
let legacyRescueFallback = false;
// Legacy mode retains the paid weighted-query admission gate. In
// Cloudflare mode the provider-cost spend rules are authoritative for
// this endpoint; Free's separate two-message lease remains above.
let usage: Awaited<ReturnType<typeof trackUsage>> | null = null;
if (!cloudflareGateway) {
const ipAddress = request.headers.get('cf-connecting-ip') || undefined;
usage = await trackUsage(env, authResult.deviceId, usageTier, authResult.userId, ipAddress, body.model);
}
if (usage && !usage.allowed) {View on GitHub (pinned to 4ebf712990)
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.
Example fix
// before: tight retry loop
for (;;) { await fetch(GATEWAY, { method: 'POST', body }); }
// after: respect backoff on 429
for (;;) {
const res = await fetch(GATEWAY, { method: 'POST', body });
if (res.status !== 429) break;
const retryAfter = Number(res.headers.get('retry-after') ?? 30);
await new Promise(r => setTimeout(r, retryAfter * 1000));
} Defensive patterns
Strategy: retry
Validate before calling
// client-side: throttle before calling
let lastCall = 0;
async function throttledCall(fn, minIntervalMs = 3000) {
const wait = lastCall + minIntervalMs - Date.now();
if (wait > 0) await new Promise(r => setTimeout(r, wait));
lastCall = Date.now();
return fn();
} Try / catch
const res = await fetch(GATEWAY, opts);
if (res.status === 429) {
const retryAfter = Number(res.headers.get('retry-after') ?? 30);
await sleep(retryAfter * 1000 * (1 + Math.random() * 0.5)); // jittered backoff
return retry();
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- credits_exhausted | daily_limit_exceeded
- IP abuse detected: ${ipAddress} has ${ipCount} queries today
AI-assisted analysis of screenpipe/screenpipe@4ebf712990 (2026-09-01).
Data as JSON: /api/errors/e3fe11489299e7aa.
Report an issue: GitHub.