OpenAI 429 is not always a rate limit
HTTP 429 is used for both a transient rate limit and a permanently exhausted credit balance. Classify insufficient_quota before you retry. A missing GPT signal still goes to requires_review, never a pass.
Published 2026-09-09 · Search job: insufficient_quota vs rate limit
OpenAI returns HTTP 429 for both a transient rate limit and a permanently exhausted credit balance. Those two conditions need opposite responses.
insufficient_quota and credit_balance_exhausted are permanent. Retrying them spends the retry budget on a call that cannot succeed once. A plain 429 with a Retry-After is transient. Honour the header, cap the sleep, and try again. We learned this on September 3, 2026, when the landing-page classifier parked a batch of creatives in requires_review with status: 'skipped' and confidence 0.000 while their crawls returned HTTP 200 with correct content.
What the stored reason hid
The first version of the failure text said only "returned HTTP 429." That sentence reads as a rate limit. The investigation went after burst concurrency and cache poisoning. The actual answer was an unpaid API bill. Every retry was spent on a call that could not succeed once.
Classification now reads error.type, error.code, and error.message. The message substring net sits beneath type and code, because both field names have already been renamed across API versions. Permanent quota exhaustion stops the retry loop and records the real cause.
Transient versus permanent is the same split we already needed
A 400 on a missing reporting view, retried every minute for 111 days, was the Neon compute incident. See A compute bill from a one-minute cron. The landing-page classifier now uses the same idea: retry 429, 408, 5xx, and thrown fetches up to three attempts. Honour Retry-After capped at 8 seconds, otherwise back off from 1 second with jitter so a burst does not retry in lockstep. Permanent failures (400, 401, 403, 404) are not retried.
The retry budget stays small because this runs inside a buyer-facing submission on a Worker. A long sleep would trade a recoverable requires_review for a hard timeout.
A missing GPT signal is never a pass
Quota exhaustion must not become an approval pass. A missing landing-page category signal still routes to requires_review. "We could not check" is not "we checked and it was fine." The analysis cache stores only conclusive pass and fail, so a re-submission after the provider recovers really does call the provider again.
This condition can halt creative review for every account at once while nothing throws and no job fails. It has its own operational detector, landing_page_classifier_blocked, at a threshold of 2 events in 6 hours. The detector matches on the reason text written by the permanent-failure classifier. Those two strings have to stay in sync.
The OpenAI key also lives in more than one Worker secret. Rotating one and forgetting the others leaves the classifier or the dashboard assistant broken in ways that do not throw. That is an operations note, not a buyer promise.
If a landing page crawls clean and still sits in requires_review with a skipped GPT signal, read the stored reason before you assume rate limit. insufficient_quota and a 429 look the same if you only keep the status code.