screenpipe/screenpipe · error

credits_exhausted | daily_limit_exceeded

credits_exhausted | daily_limit_exceeded

Error message

hosted AI admission rejected

What it means

Server-side log line from the gateway worker when the legacy (non-Cloudflare-gateway) daily weighted-query admission gate rejects a request: `trackUsage` returned `allowed: false`. The caller receives a 429 whose error code is `credits_exhausted` when the account has zero credits left, otherwise `daily_limit_exceeded` with used/limit/resets_at and upgrade options. Background/automation requests from paid hosted-AI plans may instead be rescued by downgrading the request body to a fallback model rather than failing.

Source

Thrown at packages/ai-gateway/src/index.ts:653

					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) {
				console.warn('hosted AI admission rejected', {
					gate: 'daily_query',
					tier: authResult.tier,
					accountPlan: authResult.accountPlan,
				});
				const creditsExhausted = (usage.creditsRemaining ?? 0) <= 0;
				const allowanceError = {
					status: 429,
					code: creditsExhausted ? 'credits_exhausted' : 'daily_limit_exceeded',
				};
				const rescueFallbackBody = resolveBackgroundFallbackBody({
					enabled: isBackgroundRequest(request) && hasPaidHostedAiPlan(authResult),
					error: allowanceError,
					body,
					env,
				});
				if (rescueFallbackBody) {
					legacyRescueFallback = true;
					body = rescueFallbackBody;

View on GitHub (pinned to 4ebf712990)

Solutions

  1. Read the 429 body: `resets_at` tells when the free daily limit resets (UTC); wait for the reset, or check `credits_remaining` to distinguish `credits_exhausted` from `daily_limit_exceeded`.
  2. Buy credits or subscribe (links are included in the 429's `upgrade_options.buy_credits` / `upgrade_options.subscribe`) to raise the effective daily allowance.
  3. Reduce consumption: lower request volume, prefer cheaper/weight-0 models (getModelWeight multiplies usage by model weight), and batch or cache results instead of re-querying.
  4. If this is a background/automation request from a paid hosted-AI plan, the gateway already attempts a fallback-model rescue; ensure the request is marked as background (and the plan qualifies) so it downgrades instead of failing.

Example fix

// before: blind retry
const res = await callGateway(body);
// after: honor daily-limit 429
const res = await callGateway(body);
if (res.status === 429) {
  const err = (await res.json()).error;
  if (err === 'credits_exhausted') throw new Error('Buy credits at screenpi.pe/onboarding');
  const waitMs = new Date(err.resets_at).getTime() - Date.now(); // daily_limit_exceeded: wait for reset
}
Defensive patterns

Strategy: fallback

Validate before calling

// before calling, check the quota endpoint / keep local counters
const res = await fetch(`${GATEWAY}/usage`);
const { used_today, limit_today, credits_remaining } = await res.json();
if (used_today >= limit_today && credits_remaining <= 0) {
  throw new Error('daily quota exhausted — upgrade at screenpi.pe/onboarding');
}

Try / catch

const res = await callGateway(body);
if (res.status === 429) {
  const payload = await res.json();
  if (payload.error === 'credits_exhausted') {
    throw new Error('no credits left: ' + payload.upgrade_url);
  }
  // daily_limit_exceeded: schedule retry at payload.resets_at
  setTimeout(() => retry(), new Date(payload.resets_at).getTime() - Date.now());
}

Prevention

When it happens

Trigger: A device's weighted daily query count (per-model weight times count) reaches `limits.dailyQueries` for its tier, or credits drop to zero; e.g. a free-tier user consuming all free AI queries for the day, or an app with no credits calling after the free allowance is spent. Anonymous requests over the IP daily limit also land here via trackUsage.

Common situations: Heavy daily usage of hosted AI on the free tier (limit resets at the next UTC day); pipe/automation loops draining the daily quota early; spoofed/shared device IDs caught by IP-based tracking; a paid subscription lapsing so the account reverts to the free limit.

Related errors


AI-assisted analysis of screenpipe/screenpipe@4ebf712990 (2026-09-01). Data as JSON: /api/errors/d680a6269d620d74. Report an issue: GitHub.