Network calls fail. Rate limits get hit, servers have bad minutes, requests are malformed, keys expire.

The OpenAI Python SDK already retries the retryable errors for you (connection problems, 408, 409, 429 and 5xx) with exponential backoff. Your job is three things: set sensible timeouts, handle the errors that retrying cannot fix, and log the request id so a failure can be traced later.

1. The exception hierarchy

openai.APIError ├── APIConnectionError -> retried automatically │ └── APITimeoutError -> retried automatically └── APIStatusError ├── 400 BadRequestError -> fix the request ├── 401 AuthenticationError -> fix the config ├── 403 PermissionDeniedError -> fix the config ├── 404 NotFoundError -> fix the request ├── 422 UnprocessableEntityError -> fix the request ├── 429 RateLimitError -> retried automatically └── 5xx InternalServerError -> retried automatically 

2. Automatic retries, observed

fake_client below is a small stub that scripts HTTP responses, so you can watch the retry logic without spending tokens or waiting for a real outage.

import openai from fake_openai import fake_client # a 429 then a 500, 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 

Three requests for one call in the first case, and the 400 went out exactly once.

3. Timeouts

import openai from fake_openai import fake_client 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 

The default timeout is 10 minutes. That suits long reasoning calls, 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 a retried timeout multiplies the wait: worst case = 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 

Order matters: APITimeoutError is a subclass of APIConnectionError, so catch it first.

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 it persists, cut 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 and 408/409/429/5xx (max_retries, default 2) with backoff.
  • Other 4xx errors mean fix your request; retrying won't help.
  • Set timeouts per use case; the default is long.
  • Log request_id and check status == "incomplete".

This is part 12 of a free 20-part OpenAI Python SDK guide. The full series (streaming, structured outputs, function calling, evals, cost) is at recallrun.dev/guides/openai-sdk.