Openai Spend Limit 429 Fail Closed Client Audit 2026
- Slug:
openai-spend-limit-429-fail-closed-client-audit-2026 - Date: 2026-07-29 (UTC)
- Track: api-backed-poc
- Evidence level: openai-api-backed-lab (live API calls + local sandbox harness)
- Environment: macOS, Python 3.12.8,
openaiPython SDK 2.37.0 - Safety boundary: no confidential, customer, credential, or private data was sent. Live prompts were the literal string
Reply with the single word: ok. No real spend limit was tripped, and no production system was touched.
What we set out to check
OpenAI documents that a hard spend limit makes requests return 429 with the
insufficient_quota code, while ordinary throughput throttling also returns
429. The question: does a normal client tell those apart, and what does a
default client do when it can't?
Three probes:
- Local retry harness — a fake OpenAI endpoint that always answers
429, once with the spend-limit body and once with the rate-limit body. Count how many HTTP requests one SDK call actually produces. - Discriminator probe — inspect the raised exception to see which fields, if any, separate the two cases.
- Live header probe — one tiny real call to each of
/v1/responsesand/v1/chat/completionsto see what limit signalling a healthy200carries.
Probe 1 — local retry harness (zero cost, no OpenAI traffic)
Script: /tmp/effloow-429-lab/probe_local.py (local http.server on
127.0.0.1:8137, SDK pointed at it via base_url, fake key
sk-local-fake-not-a-real-key).
python3 probe_local.py
Output:
{
"insufficient_quota": {
"http_requests_sent": 3,
"wall_clock_seconds": 1.56,
"request_offsets_seconds": [0.35, 0.78, 1.562],
"exception": "RateLimitError"
},
"rate_limit_exceeded": {
"http_requests_sent": 3,
"wall_clock_seconds": 2.01,
"request_offsets_seconds": [0.002, 1.01, 2.013],
"exception": "RateLimitError"
},
"insufficient_quota_max_retries_0": {
"http_requests_sent": 1,
"wall_clock_seconds": 0.0
},
"_env": { "openai_sdk": "2.37.0", "python": "3.12.8" }
}
Observations:
- One application-level call became 3 HTTP requests in both cases: the original plus the SDK's two default retries.
- The SDK raised the same
RateLimitErrorclass for both bodies. - With
retry-after: 1present, retries landed ~1.0s apart (0.002 / 1.01 / 2.013). Without it, the SDK used its own backoff (0.35 / 0.78 / 1.562). max_retries=0produced exactly 1 request.
Probe 2 — what distinguishes the two 429s
Script: /tmp/effloow-429-lab/probe_discriminator.py, max_retries=0.
{
"insufficient_quota": {
"exception_class": "RateLimitError",
"status_code": 429,
"e_code": "insufficient_quota",
"e_type": "insufficient_quota",
"body_error_code": "insufficient_quota",
"has_retry_after_header": false
},
"rate_limit_exceeded": {
"exception_class": "RateLimitError",
"status_code": 429,
"e_code": "rate_limit_exceeded",
"e_type": "requests",
"body_error_code": "rate_limit_exceeded",
"has_retry_after_header": true
}
}
Observation: status code and exception class are identical. The usable
discriminator is error.code on the exception body (e.code). The presence of
a retry-after header is a secondary signal in this harness, but it is set by
our fake server, so treat it as illustrative rather than as verified OpenAI
behaviour.
Probe 3 — live limit headers on a healthy response
Two real calls, model gpt-5.5-2026-04-23, max_retries=0, budget-guarded via
scripts/proof_budget.py. Artifact: openai-spend-limit-429-fail-closed-client-audit-2026.openai.json.
/v1/responses — HTTP 200, usage 29 total tokens. Limit-related headers found:
{ "x-request-id": "28bed0ce-b8b6-46eb-9d40-f68dca0e2b2d" }
No x-ratelimit-* header at all.
/v1/chat/completions — HTTP 200, usage 29 total tokens, same account, same
model, same session:
[
"x-ratelimit-limit-requests",
"x-ratelimit-limit-tokens",
"x-ratelimit-remaining-requests",
"x-ratelimit-remaining-tokens",
"x-ratelimit-reset-requests",
"x-ratelimit-reset-tokens"
]
Daily token ledger after both live calls: 58 tokens.
What worked / what did not
- Worked: reproducing the retry multiplication and the exception-field discriminator deterministically, with no OpenAI spend.
- Worked: observing a real endpoint-to-endpoint difference in limit headers.
- Not attempted: tripping an actual OpenAI hard spend limit. We did not want to
disable the account's API access, so the
insufficient_quotaresponse body used in probes 1 and 2 is the documented shape, served locally.
Limitations
- The
429bodies in probes 1 and 2 were served by our own local endpoint. They reproduce the documented shape; they are not captured OpenAI responses. - Probe 3 is one account, one model, one moment. Header presence may vary by tier, endpoint version, region, or over time. It is not a claim that the Responses API never returns rate-limit headers.
- Retry counts and backoff timings are specific to
openaiPython SDK 2.37.0 defaults. Other SDKs, gateways, and HTTP clients differ. - No production traffic, customer data, or third-party service was involved.
- The wall-clock difference between the two cases (1.56s vs 2.01s) is not a
measurement of OpenAI pacing. It is an artifact of our own fake server, which
set
retry-after: 1on the rate-limit body and no such header on the spend-limit body. Only the ~1.5s "wasted wait under SDK defaults" figure should be cited. insufficient_quotais not unique to a configured hard spend limit. The same code has long accompanied an exhausted prepaid balance ("you exceeded your current quota, please check your plan and billing details"). Both conditions are terminal and non-retryable, so the branch logic is unaffected, but alert copy should say "refused on billing grounds" rather than naming a cap.
Addendum 2026-07-29 (phase 1.5 verification)
Re-verified before publication:
- Live WebSearch confirmed the rollout announcement is real: OpenAI Developer Community thread "Hard spend limits rolling out to all API Platform accounts" and the OpenAI Devs post stating access is being expanded "to all accounts this week." Article wording set to "began expanding ... in late July 2026" to match the in-progress phrasing rather than asserting completion.
- Live WebSearch re-confirmed the spend-limits guide language on both the
429+insufficient_quotacontract and the non-instantaneous enforcement ("recorded spend can slightly exceed the configured amount").
Primary sources consulted
- OpenAI, "Spend limits" — https://developers.openai.com/api/docs/guides/spend-limits
- OpenAI, "Rate limits" — https://developers.openai.com/api/docs/guides/rate-limits
openai-pythonREADME, Retries section — https://github.com/openai/openai-python- OpenAI Cookbook, "How to handle rate limits" — https://developers.openai.com/cookbook/examples/how_to_handle_rate_limits
Read the article
This note supports the public article and records what was actually checked.