Symptom
When calling the native image generation endpoint (POST /v1beta/models/{model}:generateContent), you may see this combination:
- The dashboard log shows the request succeeded and it was billed;
- The client hangs anyway, failing only when its own read timeout fires;
- Errors look like
Read timed out,ETIMEDOUT, orUND_ERR_BODY_TIMEOUT.
It arrives in windows
This matters, because it determines how you reproduce it and how you interpret what you see:- Inside a window: consecutive calls hang, all of them, without exception;
- Outside a window: dozens of consecutive calls run perfectly, with not a single occurrence.
:streamGenerateContent) and text-only models are generally unaffected. This page is about non-streaming image generation, where the response body is large — the JSON body for a 2K image is on the order of 13 MB.Cause
Image responses are sent withTransfer-Encoding: chunked. Per the HTTP/1.1 spec, once the server has sent the last data chunk it must send a terminating chunk (a zero-length chunk) to tell the client “this is the end.”
That is the step that fails: every data chunk arrives, but the terminating chunk is never sent and the connection is never closed.
The client is left holding a complete, usable JSON document (the image base64-decodes fine), with no way to know the body is finished. So it keeps waiting — until its own read timeout fires.
An analogy: the parcel is already on your doorstep, but the courier forgot to tap “delivered.” You sit watching the tracking page for an update while the package is right outside.
Three conclusions that directly determine how to handle it:
The data is complete
Waiting does not help
Not tied to one machine
How to identify it
If all three of these hold, this is almost certainly what you are hitting:The response carries Transfer-Encoding: chunked and no Content-Length
The bytes received so far already parse as complete JSON
json.loads over what you have — it succeeds, and the inlineData.data inside base64-decodes to a complete, usable image.After that parse succeeds, no new bytes arrive for a long time
How it differs from two similar cases
All three produce similar-looking errors, but the root causes and the fixes are completely different. Do not apply one set of criteria to all of them:ECONNRESET, that is a different class of problem; see Connection Drops.
Compatibility layer: finish the response on the client side
The idea is simple: do not wait for the connection to end — finish as soon as the bytes you already have parse as complete JSON.Critical: keep a grace period
Do not finish the instant parsing succeeds. In the normal case the terminating chunk is usually in the very next TCP segment, only milliseconds away. Cutting off as soon as parsing succeeds would misclassify “the terminator was a few milliseconds late” as “the server never sent it.” The correct approach: once parsing succeeds, wait a short while longer (3–5 seconds is a good default). If any bytes arrive during that window, carry on normally. Only if nothing arrives do you declare it stalled and finish the response yourself.The checks to clear before finishing a response
Ordered from cheapest to most expensive. If any one of them fails, keep waiting — do not finish the response:Python
Read the stream on a background thread and implement the grace period with a queue timeout on the main thread:Node.js
Node.js needs no extra thread —reader.read() is already a promise, so Promise.race can cap how long you wait for the next chunk:
Choosing timeouts
The easiest mistake here is using one timeout value for two different things: “waiting for the upstream to generate the image” and “the silence between two chunks after the first byte.” Their normal durations differ by an order of magnitude. Merge them into one value and you either kill slow generation as if it were a failure, or leave genuinely stalled requests waiting for minutes.✅ Recommended
❌ Avoid
fetch in Node 18+) has three independent timeouts, and an SDK’s timeout option does not reach any of them. See the “Node.js: three independent timeouts” section of Connection Drops for the correct configuration.Retries and billing
Once you have determined the server never finished the response, work through this order:Use the image you already have — this ends it in almost every case
Retry only if parsing genuinely failed
Back off after repeated failures instead of retrying tightly
Rollout notes
These are language- and framework-agnostic, drawn from our own rollout:- Put it at a single network-layer entry point, not scattered across call sites. Make it part of the “send a request” action itself. That covers every image path at once, leaves business code untouched, and means there is only one place to change once the server side is fixed.
- What you actually need is incremental read access. The prerequisite is being able to see what has arrived before the response ends. Nearly every HTTP client offers this (streaming reads, chunk callbacks, progress events), but it is usually not the default — the default “just give me the whole body” is precisely the path that hangs. This is where most of the work is.
- Time from the last chunk received, not from the start of the request. Reset the grace-period timer on every chunk. That way you neither punish slow networks nor miss the “nothing is moving at all” state.
- Put it behind a switch. Keep the behavior behind a flag you can turn off at any time. If anything unexpected shows up right after release, flip it off to restore the old behavior — no emergency deploy needed.
- Add telemetry. Log every time the completion path fires (timestamp, byte count, wait duration). It serves three purposes: quantifying how often this actually happens, confirming the layer is doing its job, and confirming the counter drops to zero after the server-side fix — which is the only objective basis for deciding the layer can be retired.
- A bonus improvement while you are in there. Once you have incremental reads, you can surface real progress to users (“receiving data, X.X MB”). That long download was previously a complete black box on their side.
How we deployed it ourselves
We have already completed and verified this work on our own AI image studio (imagen.apiyi.com). Using a mock service that reproduces the fault (sends the full body, then neither signals the end nor closes the connection), we ran this comparison:
Common questions
Could this cut off a request that was actually healthy?
Could this cut off a request that was actually healthy?
Could I end up with half an image?
Could I end up with half an image?
Isn't this just papering over a server-side problem?
Isn't this just papering over a server-side problem?
Should we remove it once the server side is fixed?
Should we remove it once the server side is fixed?
When to contact support
If, after adding the compatibility layer above, any of the following still holds, gather your materials and contact support:- The bytes you have never parse into complete JSON (this is not the scenario on this page — the transfer really was cut short);
- Even with client-side completion, you receive no response headers at all for a long time (that means the upstream has not started sending yet — slow generation or an upstream fault, not a completion problem);
- The stall rate stays consistently high rather than clustering in time windows, reproducing steadily over a long period.
x-request-id, the call time (with timezone, e.g. 2026-08-03 13:15 (UTC+8)), the model name and key parameters such as imageSize, the raw client-side exception, and how many bytes you had received when it hung.
Related documentation
Connection Drops
ECONNRESET, SSL EOF, undici’s three timeouts, and the local-proxy diagnostic matrix