You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
bug: Batch API result file download fails for large outputs (>200MB) with ConnectionResetError #2959
Confirm this is an issue with the Python library and not an underlying OpenAI API
This is an issue with the Python library
Describe the bug
Describe the bug
When using the Batch API to download large result files (specifically when the output .jsonl exceeds ~200-300MB, e.g., 50k embedding rows), the download connection is prematurely closed by the peer.
The error typically manifests as:
httpcore.RemoteProtocolError: peer closed connection without sending complete message body (received X bytes, expected Y bytes)
To Reproduce
Create a Batch job with 50,000 embedding requests (text-embedding-3-small).
Wait for the batch to complete (status: completed).
Attempt to download the result file using the SDK:
Python
Standard SDK approach that fails
content = client.batches.retrieve_content(batch.output_file_id)
content.with_streaming_response.content()
Or using the requests library directly with the file URL.
The download will consistently fail after receiving a few hundred megabytes.
Expected behavior
The SDK should handle large file streaming robustly, or provide a built-in chunked download/retry mechanism for massive Batch outputs (1GB+).
Environment
OS: macOS 15.x
Python Version: 3.12.x
Additional context
Workaround: I've confirmed that splitting the 50k requests into smaller batches of 10k (resulting in ~200MB files) allows for stable downloads. This suggests a potential timeout or buffer limitation on the server-side proxy or within the SDK's streaming implementation for payloads exceeding a certain threshold.
I am currently implementing a manual chunked-download helper with exponential backoff to bypass this. but i dont think it;s optimal
To Reproduce
To Reproduce:
Prepare a batch with ~50,000 rows (near the OpenAI limit)
Submit to Batch API with endpoint /v1/embeddings
Wait for status = completed
Attempt to download result file via Files API Code Snippets:
sample code:
from openai import OpenAI
client = OpenAI()
Result file is ~1GB when batch has 50,000 embeddings
#failed on streaming :
with client.files.with_streaming_response.content(result_file_id) as resp:
with open("result.jsonl", "wb") as f:
for chunk in resp.iter_bytes(chunk_size=1024 * 1024):
f.write(chunk)
raises: peer closed connection without sending complete message body
This is a significant issue for production systems handling large outputs. Any ETA on a fix? In the meantime, could chunking the download help mitigate this?
You can also try pyresilience, to add retry/timeout around the download operation. This library have all the 7 resilience patterns, wit 1 decorator.
Feel free to open PR, if you need any further assistance, do let me know.
Answer preview: "This does not look like a Python-SDK-specific bug. The strongest evidence is in the report itself: the same failure happens with raw requests.get(..., stream=True), not just with openai-python the error is a transport-level disconnect / incomplete body: httpcore.RemoteProtocolErr"
I'd like to take this one. Quick summary of the direction I plan to pursue, building on what's already established in this thread:
The failure is a mid-transfer cutoff on a long-lived single connection (it reproduces with plain requests, and the reporter's chunked Range workaround works). So restarting the download from byte 0 — which is what max_retries does today for client.files.content() — can never make progress when a transfer dies at a deterministic size/duration threshold: every retry attempt gets killed at roughly the same point.
Plan: teach the retry loop in _base_client.py to resume an interrupted non-streamed GET download with Range: bytes=<received>- when the server advertised Accept-Ranges: bytes, instead of restarting from zero:
attempt 1 behaves exactly as today (full download);
if the body read dies mid-transfer with a retryable transport error, subsequent attempts send a Range header for the bytes already received; a 206 response appends to the partial body, a 200 response (server ignored the range) replaces it;
@jbeckwith-oai friendly nudge on this issue — I've opened a candidate fix in #3764 and would appreciate a steer on whether the shape is acceptable before it goes further.
Quick summary of what it does and doesn't do:
Retries for non-streamed GETs currently restart a failed download from byte 0, which can never recover when a long transfer is cut at a fixed size/duration threshold (the repro in this thread). The PR makes retries resume via Range + If-Range when — and only when — the origin advertised Accept-Ranges: bytes and served a strong validator; everything else keeps today's behaviour exactly. No public API changes. 29 focused tests, sync + async.
Two things I'd be grateful for: the PR's workflows need an approval to run (first-time contributor here), and if you'd rather see this implemented at the template/generator level — or not in the SDK at all — that's a perfectly good answer and I'm happy to hand over the analysis and tests, or rework the PR in whatever direction you prefer.
PR #3764 proposes a fix for this with byte-range resume of interrupted downloads (works when the server advertises Accept-Ranges: bytes) — linking here for anyone tracking this issue.
Activity
passionworkeer commentedon Mar 11, 2026
This is a significant issue for production systems handling large outputs. Any ETA on a fix? In the meantime, could chunking the download help mitigate this?
xwang049 commentedon Mar 11, 2026
chunking to 200MB is working for sure, but this shouldn't be like this low threshold
fix(files): stream content in 1 MB chunks to prevent ConnectionResetE…
AhsanSheraz commentedon Mar 24, 2026
Hey @xwang049 @passionworkeer ,
You can also try pyresilience, to add retry/timeout around the download operation. This library have all the 7 resilience patterns, wit 1 decorator.
Feel free to open PR, if you need any further assistance, do let me know.
Pypi: https://pypi.org/project/pyresilience/
Docs: https://pyresilience.readthedocs.io
khalidsaidi commentedon Mar 25, 2026
A2ABench has an accepted answer for this imported thread.
ai villageCacinieP commentedon Aug 31, 2026
I'd like to take this one. Quick summary of the direction I plan to pursue, building on what's already established in this thread:
requests, and the reporter's chunkedRangeworkaround works). So restarting the download from byte 0 — which is whatmax_retriesdoes today forclient.files.content()— can never make progress when a transfer dies at a deterministic size/duration threshold: every retry attempt gets killed at roughly the same point.LegacyAPIResponse.contentto loopiter_bytes()in 1 MB chunks, but httpx already reads the body incrementally, so that doesn't change the network behavior (and as a side effect it makes.contentfail on a second access).Plan: teach the retry loop in
_base_client.pyto resume an interrupted non-streamed GET download withRange: bytes=<received>-when the server advertisedAccept-Ranges: bytes, instead of restarting from zero:Rangeheader for the bytes already received; a206response appends to the partial body, a200response (server ignored the range) replaces it;Accept-Ranges: byteskeep today's restart-from-zero behavior unchanged;This keeps the existing
max_retriessemantics — it just makes retries actually make forward progress for large file downloads.CacinieP commentedon Sep 1, 2026
@jbeckwith-oai friendly nudge on this issue — I've opened a candidate fix in #3764 and would appreciate a steer on whether the shape is acceptable before it goes further.
Quick summary of what it does and doesn't do:
Range+If-Rangewhen — and only when — the origin advertisedAccept-Ranges: bytesand served a strong validator; everything else keeps today's behaviour exactly. No public API changes. 29 focused tests, sync + async.iter_bytes()reads, which doesn't change the network behaviour; this one changes what retries actually request.src/openai/lib/_resumable.py; the template-owned_base_client.pychange is down to ~70 lines of retry-loop integration.Two things I'd be grateful for: the PR's workflows need an approval to run (first-time contributor here), and if you'd rather see this implemented at the template/generator level — or not in the SDK at all — that's a perfectly good answer and I'm happy to hand over the analysis and tests, or rework the PR in whatever direction you prefer.
fix: resume interrupted file downloads with byte-range retries
CacinieP commentedon Oct 1, 2026
PR #3764 proposes a fix for this with byte-range resume of interrupted downloads (works when the server advertises
Accept-Ranges: bytes) — linking here for anyone tracking this issue.