OpenAI SDK: Errors, retries & timeouts
Network calls fail. Rate limits are hit, servers have bad minutes, requests are malformed, keys expire.
In this post
What this post covers
Network calls fail. Rate limits are hit, servers have bad minutes, requests are malformed, keys expire. The SDK already retries the retryable errors automatically (connection problems, 408, 409, 429 and 5xx) with exponential backoff. Your job is to set sensible timeouts, handle the errors that retrying cannot fix, and log the request id so problems can be traced.
1. The exception hierarchy
2. Automatic retries, observed
import openai
from fake_openai import fake_client
# two 429s then success: the SDK retries with backoff and you never see the errors
client = fake_client(script=[{"status": 429}, {"status": 500}, "Recovered."], max_retries=2)
r = client.responses.create(model="gpt-5.6-luna", input="hello")
print(r.output_text, "| HTTP requests made:", len(client.fake.requests))
# more failures than retries: the error reaches your code
client = fake_client(script=[{"status": 429, "message": "Rate limit reached for requests"}] * 5, max_retries=2)
try:
client.responses.create(model="gpt-5.6-luna", input="hello")
except openai.RateLimitError as e:
print(type(e).__name__, e.status_code, "| request id:", e.request_id,
"| requests made:", len(client.fake.requests))
# a 400 is NOT retried: sending the same bad request again would fail again
client = fake_client(script=[{"status": 400, "message": "Invalid value for 'model'"}], max_retries=2)
try:
client.responses.create(model="gpt-5.6-luna", input="hello")
except openai.BadRequestError as e:
print(type(e).__name__, e.status_code, e.message, "| requests made:", len(client.fake.requests))
Output
Recovered. | HTTP requests made: 3
RateLimitError 429 | request id: req_fake_8 | requests made: 3
BadRequestError 400 Error code: 400 - {'error': {'message': "Invalid value for 'model'", 'type': 'fake_error', 'code': None}} | requests made: 1
3. Timeouts
import openai
from fake_openai import fake_client
fake_client().responses.create(model="warm-up", input="x") # one-time import cost
client = fake_client(script=[{"delay": 2.0}, "too late"], timeout=0.5, max_retries=0)
try:
client.responses.create(model="gpt-5.6-luna", input="summarise this long report")
except openai.APITimeoutError as e:
print("timed out:", type(e).__name__)
# per-call override for a known-slow request (e.g. high reasoning effort)
client = fake_client(script=[{"delay": 0.8}, "done"], timeout=0.5, max_retries=0)
r = client.with_options(timeout=2.0).responses.create(model="gpt-5.6-luna", input="hard question")
print("with a longer timeout:", r.output_text)
Output
timed out: APITimeoutError
with a longer timeout: done
That suits long reasoning calls, but not a web request where a user is waiting. Set a timeout per use case (interactive: 30–60 s; background: longer), stream interactive responses so users see progress, and remember that a retried timeout can double the wait. Budget total time = timeout × (retries + 1).
4. A handling pattern for applications
import logging
import openai
log = logging.getLogger("assistant")
def answer(question: str) -> str:
try:
r = client.responses.create(model=MODEL, input=question, max_output_tokens=800)
except openai.RateLimitError:
return "We're busy right now. Please try again in a minute." # already retried by the SDK
except openai.APITimeoutError:
return "That took too long. Try a shorter question."
except openai.APIConnectionError:
log.exception("network problem reaching OpenAI")
return "Temporary connection problem."
except openai.BadRequestError as e:
log.error("bad request %s: %s", e.request_id, e.message) # a bug in OUR code
return "Sorry, something went wrong."
except (openai.AuthenticationError, openai.PermissionDeniedError):
log.critical("OpenAI credentials rejected") # page someone
raise
if r.status == "incomplete":
log.warning("incomplete response %s: %s", r.id, r.incomplete_details)
return r.output_text
| Error | Meaning | Action |
|---|---|---|
| 401 / 403 | bad, revoked or under-privileged key | alert; never retry in a loop |
| 400 / 422 | your request is invalid (parameters, schema, too long) | fix the code; log the request id |
| 404 | unknown model, file or response id | check names and ids (models get retired) |
| 429 | rate limit or quota exceeded | retried automatically; if persistent, reduce concurrency or raise limits; check billing |
| 5xx, connection, timeout | transient server or network trouble | retried automatically; degrade gracefully if it persists |
Recap
- The SDK retries connection errors, 408/409/429/5xx (
max_retries, default 2) with backoff. - 4xx errors other than 408/409/429 mean fix your request; retrying will not help.
- Set timeouts per use case; the default is long.
- Log
request_idand checkstatus == "incomplete".
Get the free RAG production checklist
25 checks to run before real users see your RAG feature: exact-match misses, invented citations, prompt injection through documents, quality that slips after a prompt change. Confirm your email and the PDF comes with the welcome email, plus each new experiment.