Skip to content

stream error: stream disconnected before completion: stream closed before response.complete; retrying

Two things in this line are doing real work, and neither is the word “disconnected”. The tail names a specific missing event — response.complete, the terminal event that ends a well-formed streamed response — and the last word says the client has already started a retry on its own. So this is not an error the platform sent you: documented API failures arrive either as an HTTP status before the stream begins or as a typed stream event carrying a code and a message, and you received neither. Your account, your key and your request are not implicated by this string at all.

The interesting part is what is missing

A streamed response has two endings. There is the byte-level ending, where the HTTP body terminates, and there is the semantic ending, where the terminal completion event arrives and tells the client the response is whole. This message says the first one happened and the second one did not.

That is a narrower claim than “the connection broke”, and it excludes a specific, common failure. If the transport had been cut mid-body, the HTTP layer would have complained that it was still owed bytes, which is a different error with a different diagnosis — see Response payload is not completed for that one, including the byte-count-versus-elapsed-time measurement that identifies the hop. Here the stream was allowed to end. It just ended early, in a way that looked orderly to everything except the code counting events.

Now the part people miss. There was also no error event. The Responses API has documented failure events for exactly this situation — an error event with type, code, message, param and sequence_number, and a response.failed event carrying the whole response object with an error field on it. Had either arrived, your client would have had a code to print, and it would have printed it. It printed the absence instead.

So the usual next step for a vague streaming error — improve the logging, catch the typed event, recover the real cause — produces nothing here. That advice is right for the generic streaming error string, where the details existed and a blanket handler discarded them. In this case the details never existed. Recognising that saves you the afternoon you would otherwise spend instrumenting a code path that has nothing to report.

The absence is itself the finding, and it points somewhere. A generator that decides to stop has something to say about why, and the protocol gives it two ways to say it. A stream that stops without saying anything was, more often than not, stopped by something that was not generating it — an intermediary, a connection pool, a network event. That narrows where to look without telling you which one, which is what the next two sections are for.

Is retrying useful?

Yes — and the client is already doing it, which changes your question.

The word “retrying” means the failure was handled. A single occurrence in a log, followed by a successful response, is a transient condition that the client absorbed exactly as designed. There is nothing to fix and no ticket to file for one of these. The reader who arrives here after seeing the message once is usually looking for a problem that does not exist.

What matters is the rate. Track how many turns produce this line as a fraction of turns that complete. An occasional line during long sessions is normal wear on any long-lived streaming connection. A line on most turns, or several per turn, means the retry is papering over something structural and you should stop treating it as noise.

Where the client’s retry stops being free is when it does not terminate. If attempt after attempt closes before the completion event, the condition is not transient, and every attempt re-sends the full input — failed requests still count against your per-minute limits, so a retry loop against a saturated path is actively making the situation worse. Stop the loop yourself, and diagnose.

The retry you did not write can still cost you

This is the practical hazard specific to the ; retrying suffix, and it is easy to miss because the retry is invisible from your code.

OpenAI’s documentation is explicit that you should not automatically replay a request after consuming output, and the official Python client states that stream consumption is not automatically retried precisely because replaying a request could duplicate output already delivered to your application. A client that retries a partially consumed stream is doing the thing the documentation warns against — and it is doing it on your behalf.

If anything downstream acted on the partial output before the cut, the retry makes it happen twice. Files written, tools invoked, messages posted, commits made. When you see this line in a session where work was already being applied, check the work, not just the final answer. A truncated stream that regenerates cleanly can still leave two copies of a side effect behind it, and that damage survives a successful retry.

Telling the causes apart

Does the cut land at a wandering point, or the same place every time? Log how much output arrived before each occurrence. Random points across attempts mean transient variance and the retry is the correct handling. A consistent point means something deterministic — and a deterministic stop with no error event is almost always an intermediary, because a generator failing deterministically would emit a code.

Does it correlate with long turns? If only turns that run past some duration produce it, a timer on the path is ending connections it considers idle. Long streamed responses look idle to a naive proxy, which sees a trickle rather than a request-response pair.

Does it disappear on another network? Run the same workload from a different path — a home connection, a cloud shell, a phone hotspot. Corporate egress gateways, inspecting proxies and VPN clients all terminate long-lived connections on their own schedule, and this test rules them in or out in a single session.

Did it start after an infrastructure change? A gateway upgrade, a new container image, a changed ingress config. Suspect the change before the vendor, and roll it back far enough to prove which side owns the failure.

Is it every turn, from every network, immediately? Then it is not your path. Check the platform’s status page before spending more time on your own configuration.

Fix by scenario

  • Rare, wandering, followed by success — nothing. This is the retry earning its place. Do not tune anything.
  • Correlates with turn length — raise the idle and read timeouts at every hop, and verify each from the running configuration rather than the file you edited. One of three timers fixed is why the change appears to do nothing.
  • Same cut point every time — stop retrying and look at intermediaries. Disable response buffering and compression on the streaming route; a hop that accumulates the body before forwarding it converts a healthy stream into one long silence and one burst.
  • Only on one network — the appliance in the middle is the suspect, and no amount of client configuration will out-argue it.
  • It never terminates — treat the loop as the problem. Cap the attempts yourself, capture what the client received on the last one, and check whether the error body is the thing that is unreadable rather than absent; that case is failed to parse ErrorResponse.

How to confirm it’s fixed

Absence of the message is weak evidence, because the message was intermittent to begin with. Make the test proportional instead: run a workload of the same shape and length that produced the failures, and compare the rate of occurrences per completed turn against what you measured before. Zero occurrences in a short session proves nothing.

If you changed a timeout, add one falsifiable detail: record the duration of the longest turn that now completes and confirm it exceeds the duration at which turns used to be cut. A completed run that stayed under the old threshold has not tested your change.

The neighbours in this cluster are distinguished by what the client was holding when it gave up. When the transport says it was still owed bytes rather than events, read Response payload is not completed, which is a framing violation and keys off a different measurement. When the error was thrown, caught and flattened into a sentence with no code attached, An error occurred during streaming is the page, and there the details are recoverable — unlike here. When the request never reached the endpoint at all, the message names the URL instead of an event; see error sending request for url. If you cannot tell which of them you are looking at, the AI coding error triage tool sorts them by the signals used above: whether an event or a byte count went missing, whether any error code survived, and whether the failure point moves between attempts.