openai.error.APIConnectionError: Error communicating with OpenAI: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
Nothing came back. RemoteDisconnected('Remote end closed connection without response') is the precise statement that the socket was closed after your
request had been written and before a single byte of a status line arrived.
There is no HTTP status here, which rules out every failure that is delivered
as a response: a rejected key returns 401, a quota or pacing refusal returns
429, an oversized prompt returns 400, and a model with no capacity returns 503
with server_is_overloaded. You are holding a transport failure, and the
account-side things you are about to check are all exonerated by the absence of
a status code.
Data as of 2026-09. Vendor limits and defaults change; check the official docs for current values before acting on any number below.
The inner exception is Python’s HTTP stack, not OpenAI
APIConnectionError is the class the SDK raises when a request never became a
response — it sits outside the status-code hierarchy entirely, alongside
APITimeoutError for the read-timeout case. Everything after the colon is
quoted from a lower layer. Connection aborted plus RemoteDisconnected is the
standard library reporting that it read zero bytes from a connection it believed
was open.
That inner text is the most useful part of the string, because it is specific. A certificate the runtime will not trust, a hostname that does not resolve, and a port that actively refuses all surface as different inner exceptions on the same outer class. Seeing this particular one narrows the field before you touch anything: something accepted a TCP connection, took your request, and then hung up without speaking. Middleboxes do that. DNS and TLS do not.
In code, the original is preserved: the exception the SDK raises keeps the
underlying transport error on __cause__. Logging only str(e) throws away the
layer that identifies the fault, which is how a team ends up with a thousand
identical log lines and no diagnosis.
The same exception means opposite things depending on when it fired
This is the part that decides what you do next, and the error string cannot tell you which case you are in.
If it fired before any output arrived, the SDK has already retried. Official
clients retry connection errors automatically — two attempts by default, with a
short exponential backoff — so the traceback you are reading is the third
failure, not the first. That changes the meaning completely: an identical
request has already failed three times over the same path, which is evidence of
a persistent condition rather than a fluke. Raising max_retries here is the
cheap wrong fix. It converts a failure you see in seconds into one you see in
minutes, and it does not change the path.
If it fired while you were iterating a stream, nothing was retried at all.
The Python SDK states the rule plainly: stream consumption is not automatically
retried, because replaying a request could duplicate output already delivered to
your application. Your max_retries setting never fires once bytes are flowing.
So in this case the exception really is the first failure, and a deliberate
retry is genuinely untested ground.
The discriminator is whether you received any content before the exception. Not whether the call was a streaming call — a streaming request that dies during connection setup is still in the first population, because the stream had not started. Log the number of chunks consumed alongside the exception and this question answers itself forever after.
One smaller signal in the string itself: the module path a traceback prints
tells you which generation of the client is installed in that environment, and
therefore which except clause will actually catch. An except written against
a different import path than the one in the traceback compiles fine, imports
fine, and silently catches nothing. Copy the path from the traceback, not from a
write-up.
Is retrying useful?
Yes — but almost certainly not the retry you are about to write.
Retry once, deliberately, and treat the result as a measurement. If the second attempt succeeds immediately, you are looking at a connection that had gone stale, and the section below tells you how to confirm it. If it dies the same way at the same point, stop: you have a deterministic path problem, and further attempts only cost you time and, on a streaming workload, tokens you have already paid for.
Two rules constrain how you do it. First, check what already happened — if this
was a non-streaming call, the client burned its default retries before raising,
so your loop is attempts four through six of the same doomed request. OpenAI’s
own guidance for hand-rolled retry logic is to disable the SDK’s retries or
account for them, precisely so nested loops do not multiply request volume.
Second, do not invent a delay. There is no Retry-After to honor, because no
response arrived to carry one, and no base delay is published anywhere for you
to copy. The documented shape is exponential backoff with jitter, with a cap on
both attempt count and total retry time.
Telling the causes apart
Each of these has an observation that confirms or rules it out. Run the one that matches your symptom rather than working down the list.
Does it follow idle periods? Leave the client idle for several minutes, then send one small request. HTTP clients keep sockets pooled; a NAT table, stateful firewall or corporate proxy will drop an idle mapping without telling either end. If the request after the pause is reliably the one that dies while the next succeeds, you have found a reaped connection and you can stop here.
Does it fail at the same elapsed time every run? A consistent cut-off is a timer, not congestion — an idle or total-request timeout on a proxy in the path. The client’s own default timeout is ten minutes, so anything cutting in well before that is not the SDK.
Does it only happen on large requests or long generations? Then nothing is being reaped; something is cutting a connection that is actively in use. Suspect a request-body size cap or a response-duration cap on an intermediary.
Does every request fail from process start? Then it is policy, not chance: egress rules, a proxy that requires authentication, or a host that is blocked. Nothing about pooling or timeouts applies.
Does it reproduce on a different network? A phone hotspot settles this in a minute and is the test people skip because they are already certain the vendor is down.
Fix by scenario
- Failures follow idle gaps — shorten the client’s idle connection lifetime so it discards sockets before the middlebox does, or enable TCP keep-alive so the mapping stays warm. This is a client-side change; you usually cannot get the middlebox altered.
- Consistent cut-off time — raise the idle timeout at every hop, not just in your client. One proxy left at its default makes the other changes invisible.
- Only large payloads or long generations — stream the response so the connection is never silent for long, and shrink the request body. On a streaming call, also make sure your consumer is reading fast enough that the socket is not idle from the network’s point of view.
- Every request fails — this is a configuration fix on the network path, and
the diagnosis is the inner exception from
__cause__, not this page. - Only in a container or CI runner — the image is the suspect. A base image bump changes the certificate bundle and the proxy environment at once, and neither appears in your application diff.
- Reporting it — there is no request id to attach, because there is no
response to carry the
x-request-idheader. Record the timestamp with timezone, the idle gap before the failing request, the elapsed time before it died, how many chunks you had consumed, and the value of__cause__.
How to confirm it’s fixed
Reproduce the condition, not the request. For the idle case, leave the client idle longer than the gap that used to kill it, then send a request, and require that to work on two separate occasions — a burst of quick successes proves nothing about a failure that only appears after a pause. For the timeout case, run the workload that used to die and confirm it now runs past the old cut-off time, then verify the effective timeout value at each hop from a freshly started process rather than reading the config file you edited.
If you changed retry settings, confirm you changed the right ones: on a
streaming workload, a higher max_retries cannot be the reason things improved,
because it does not apply there at all.
Related errors
- If output had already started arriving and then stopped, you are in stream error: stream disconnected before completion territory — a partially answered request, where replaying risks duplicating what you already received.
- If the stream ended with an error object your client could not parse, see failed to parse ErrorResponse: invalid type: null, where the real failure is being hidden by the parser reporting on it.
- The Node ecosystem’s version of this same transport failure is TypeError (fetch failed) — same absence of a response, different class name, and the same “it worked minutes ago” test.
- When you cannot yet tell whether a failure is local, transport or server-side, the AI coding error triage tool sorts on the signals used above: whether a status code exists, whether a request id exists, and whether any output arrived before the failure.