Troubleshooting guide

AI feature fails in production: 429, 403, quota or retired model

A feature that worked yesterday starts failing with a 429, a 403, a 503 or a vague 'something went wrong', and nothing in your code changed. The cause is usually account state at a provider: a balance, a spend cap, a retired model, a changed API rule or a paused host. This was the most common pattern in our data, with 116 verified builder reports. Owners often learned of it from a user.

116 verified cases15 min read Updated 29 Oct 2026Last reviewed 30 Oct 2026How we collect cases

What you'll see

  • A call that worked yesterday returns 429, and retrying makes no difference for hours.
  • A 429 arrives with no Retry-After header, or the message says you have reached a usage or spend limit.
  • Requests fail only in production, with a 403 that has no JSON body, while the same key works locally.
  • The production site returns 503 after a billing threshold or a failed payment, and your health check still passes.
  • A streamed answer starts, then stops partway, even though the HTTP status was 200.
  • Users see a generic error, and you learn about it from a message rather than an alert.

Why it happens

Cause 1

Account state changed at the provider, not in your code

A prepaid balance reaches zero, a tier spend cap is hit, or a limit you set yourself is reached. Anthropic returns a 429 for a tier spend cap, with error.details.error_code set to enforced_spend_limit_reached and no retry-after header. It returns a 400 for a spend limit you set. OpenAI returns a 429 and separates the cases in error.code, for example credit_balance_exhausted, organization_spend_limit_exceeded and project_spend_limit_exceeded. Retrying does not restore access in any of these cases.

Cause 2

Retries turn a hard stop into a storm or a hang

A retry loop that treats every 429 as temporary keeps calling a provider that will refuse until the month rolls over. OpenAI notes that unsuccessful requests still count towards your per-minute limit, and that its official SDKs already retry 429 and 503, so your own loop on top nests retries. Anthropic's SDK retries twice by default and honors retry-after. If the total wait is longer than your host's function time limit or your UI timeout, the user sees a timeout instead of a message.

Cause 3

The status code is lost, or the error arrives inside a 200

Wrapping a provider error in a generic 500, or catching it and returning an empty result, hides whether you need to wait, pay or change a model. Anthropic documents that when you stream over server-sent events, an error can occur after the API has already returned 200, and standard status handling does not see it. A health check that only tests your own endpoint passes throughout.

Cause 4

The host or its firewall acts on account or traffic signals

On Vercel, Spend Management can pause production deployments when a spend amount is reached, and visitors then get a 503 deployment-paused error. Raising the amount does not unpause projects; each one is resumed by hand. Separately, Vercel's automatic DDoS mitigation can block traffic from proxies or shared networks it judged malicious. A server-to-server caller can be refused while ordinary visitors are not.

Cause 5

A model, parameter or API version is retired or changed

Anthropic gives at least 60 days' notice before retiring a publicly released model, by email and in its documentation, and requests to a retired model fail. As listed on 29 October 2026, claude-sonnet-4-5-20250929 is deprecated with retirement on 30 November 2026. Parameters change too: Anthropic removed temperature, top_p and top_k in API version 2026-10-21, and a request that sets them returns a 400. Stripe applies your account's default API version to any request or webhook endpoint that does not pin one, so a default changed in the dashboard can change what a plugin receives.

How to fix it

  1. Read the provider's status, error code and request ID before anything else

    For every failed provider call, log the HTTP status, error.type, the code field and the request ID. The code field is error.code at OpenAI and error.details.error_code on Anthropic's spend-cap response. That entry tells you whether to wait, add credit or change the request.

    const res = await fetch(url, init);
    if (!res.ok) {
      const body = await res.clone().json().catch(() => ({}));
      console.error({
        status: res.status,
        type: body?.error?.type,
        code: body?.error?.code ?? body?.error?.details?.error_code,
        requestId: res.headers.get("request-id"), // Anthropic's header name
      });
    }
  2. Retry only what can recover, inside a fixed time budget

    Retry 429, 500, 503, 504 and 529 a small number of times. Treat Retry-After as a floor (it is either a number of seconds or an HTTP date), add jitter, and stop when the total wait would pass your budget. Keep that budget below your host's function time limit and your UI timeout. If you use a provider SDK, set its own retries to zero or count them, so the loops do not nest.

    const RETRYABLE = new Set([429, 500, 503, 504, 529]);
    
    async function callWithBudget(fn: () => Promise<Response>, budgetMs = 8000) {
      const start = Date.now();
      for (let attempt = 0; ; attempt++) {
        const res = await fn();
        if (res.ok || !RETRYABLE.has(res.status)) return res;
        const hint = Number(res.headers.get("retry-after")); // NaN for an HTTP date
        const wait = Math.max(Number.isFinite(hint) ? hint * 1000 : 0, 2 ** attempt * 500)
          + Math.random() * 250;
        if (attempt >= 2 || Date.now() - start + wait > budgetMs) return res;
        await new Promise((r) => setTimeout(r, wait));
      }
    }
  3. Keep the provider's status through error wrapping and tell the user what to do

    Carry the status and code in your own error type, and map them to a plain message with a next step. A spend-cap or balance error is yours to fix, not the user's, so the message can say 'this feature is unavailable, and we have been notified'. A 429 with Retry-After can say 'try again in a minute'. Do not show provider text to users.

    class ProviderError extends Error {
      constructor(
        readonly status: number,
        readonly code: string | undefined,
        readonly requestId: string | null,
        message: string,
      ) {
        super(message);
      }
    }
  4. Treat a stream that ends without its final event as failed

    A 200 does not mean the answer completed. With Anthropic's streaming API, a finished message ends with a message_stop event, so a stream that closes without one has failed. Once output has reached the user, do not replay the request automatically, as OpenAI also advises. Show the partial text with a retry button.

    let complete = false;
    for await (const ev of events) { // your parsed SSE events
      if (ev.type === "message_stop") complete = true;
    }
    if (!complete) throw new Error("stream ended before message_stop");
  5. Set limits and alerts that reach a person, and test the channel

    Set a spend limit at the provider below your tier cap. OpenAI's spend alerts only notify and traffic continues, while a hard limit makes requests return 429, so you need both. On Vercel, Spend Management notifies at 50%, 75% and 100% of the amount and can call a webhook. Check whether Pause Production Deployments is on, because it takes the production site offline. Send alerts and billing email to an address more than one person reads, and run a scheduled canary that makes one cheap real provider call.

  6. Keep model names in config, with a fallback and an audit

    Do not scatter model IDs through the code. Read them from one config, name a fallback that is also active, and check the provider's deprecations page on a schedule. On Anthropic, Usage in the Console can export a CSV of usage by API key and model, which finds code still calling a deprecated model.

    {
      "chat": { "model": "claude-sonnet-5-5", "fallback": "claude-sonnet-5" }
    }
  7. For an empty 403 or a 503 from the host, look at the host first

    A 403 from the Anthropic API is a JSON error with type permission_error, so a 403 without that body was more likely produced by something in front of your call. On Vercel, check the project's Firewall tab. Pro and Enterprise teams can add a System Bypass Rule for the caller's IP or CIDR range, or pause system mitigations for 24 hours during a critical event. For a deployment-paused 503, fix the billing cause, then resume each project.

Check it's fixed

  • Force each failure in staging: a key whose spend limit is already reached, an unknown model ID, and a stub that returns 429 with Retry-After: 120. The user gets a plain message with a next step, and the response returns inside your retry budget.
  • Every failed provider call in your logs shows status, error type, code and request ID, and a count by status is on a dashboard someone opens.
  • Send a test alert through the same channel the real spend and balance alerts use, and confirm a named person receives it.
  • Cut a streamed response off in the middle in staging. The UI shows the partial text and a retry, not a silent blank.

Fix it with Gemmein

Gemmein refuses an AI call with a named err.code, in a set order, and no refusal above the spend has cost a credit. A person short of credits is refused without the provider being called, and a call refused in Live leaves an owner's Logs row naming who, which tool and why.

  1. Branch on err.code

    Branch on err.code; on g.ai.text and g.ai.runText, provider_error carries the provider's own non-2xx status and message. Raw g.ai.chat calls are off by default for every provider key and answer 403 raw_calls_off until the owner switches "raw calls" on for that provider's key on the AI page; gemmein dev with no key keeps raw calls open, so do not read that as the cloud's answer.

    try {
      const res = await g.ai.chat({ model: "gpt-4o-mini", stream: true,
        messages: [{ role: "user", content: text }] })   // raw: needs the switch
    } catch (err) {
      if (err.code === "raw_calls_off") { /* 403: call a named tool with g.ai.run, or the owner flips the switch */ }
      if (err.code === "ai_not_configured") { /* 409: no key set for the provider; show err.message */ }
      if (err.code === "credits_exhausted") { /* 402: show the credit pack */ }
      if (err.code === "ai_capped") { /* 429: wait for resetAt */ }
      if (err.code === "provider_unreachable") { /* no answer before the first byte; the credit is refunded; retry */ }
    }
  2. Know which refusal a call meets first

    The route refuses in this order: session → rate → body size → the named tool → the provider → raw_calls_off → the model → the spend (credits_exhausted). So a raw call to a provider with no key answers ai_not_configured, not raw_calls_off, and no refusal above the spend has cost a credit.

  3. Retry on a refund, wait on the cap

    If the provider fails before its first byte, the tool's full price is refunded, the answer carries x-gemmein-credit: refunded and the provider's own status, and the person can retry. If one person makes more than 20 calls in a minute, the call answers 429 ai_capped with resetAt and nothing is spent; wait for resetAt.

  4. Check the owner's Logs row

    When a call is refused in Live, the person reads a neutral sentence and the owner's Logs row names who, which tool and why. ai_not_configured and tool_incomplete are the owner's to fix, so their message is the customer's: "This feature isn't available right now."

Questions

How do I tell a rate-limit 429 from a spend-cap 429?

On Anthropic, a spend-cap 429 has no retry-after header and error.details.error_code is enforced_spend_limit_reached; a spend limit you set returns a 400 instead. On OpenAI, read error.code: credit_balance_exhausted and the spend-limit codes need a fix at the provider, and retrying will not help.


Why does my key work locally but return 403 in production?

Check the body first. An API 403 from Anthropic is JSON with type permission_error, so a 403 without it more likely came from your host's firewall, a proxy or another layer in front. On Vercel, look at Firewall activity and consider a System Bypass Rule if you are on Pro or Enterprise.


Should I retry a 529 or a 503 overloaded error?

Yes, a few times with exponential backoff and jitter, honoring Retry-After when present, and within a total time budget. Anthropic describes 529 as temporary overload, and OpenAI's 503 server_is_overloaded means the model lacks capacity at that moment.


How much warning do I get before a model is retired?

Anthropic states at least 60 days' notice for publicly released models, sent by email to customers with active deployments and published in its deprecations table. Other providers differ, so check each one's page and send its notices to a mailbox someone reads.


Can a Stripe API version change break my webhooks or plugin?

Requests use your account's default API version unless the code or SDK pins one. Webhook events use the version set when the endpoint was created, or the account default. Stripe's upgrade guide has you create a new endpoint on the new version, tell the two apart by a query parameter, and keep the handler idempotent while both run.


Sources

Last reviewed 30 Oct 2026. What changed: No changes: steps re-checked against 8 official page(s). Checked against: platform.claude.com, platform.claude.com, platform.claude.com, developers.openai.com, developers.openai.com, vercel.com, vercel.com, docs.stripe.com. How we check this

← Back to the full report