Skip to main content

Short answer

The console log’s duration and your client’s timeout do not measure the same window.
  • The log’s duration runs up to the point where the gateway finishes processing;
  • Your client is not done until the last byte of the response body has arrived and the connection signals that it is finished.
So “the log says it completed in 280 seconds, but my 600-second timeout still didn’t get me any data” is entirely possible — and that request really did succeed and really was billed. The gap falls in the segments the log never covered.This page shows you how to measure that gap, pin it to a specific segment, and treat the right cause.
This page is about non-streaming calls with large responses: image endpoints returning base64 are the classic case (response bodies run from several MB to tens of MB), and non-streaming long-text output behaves the same way. Streaming calls and small responses are generally unaffected.

What the log’s duration actually covers

A call’s total latency breaks into five segments:
The gap does not appear in any log field.We ran a controlled raw-socket comparison internally: for the same batch of requests our backend recorded a 5-second duration with a success status, while the client actually waited 37–40 seconds for the complete response. Those 31–35 seconds happened after the gateway finished processing, and no duration field records them.Which means: quoting the log duration to refute “it took forever on my side” proves nothing — the two numbers were never in conflict. Locating the problem requires per-segment timing on the client.
Fields available in the console and in the Log Query API: duration_for_view (call duration in seconds), is_stream, and request_id (quote this one when reporting an issue).

Step 1: pin the gap to a segment with one curl

This is the entry point for everything below. Run it first, then decide which section applies.
Three derived metrics turn those raw numbers into meaningful segments:

Reading the result

Match your numbers against this table — it decides what you do next:
One run is not enough. This class of problem arrives in time windows — inside a window every call in a row is affected, outside it dozens of calls in a row are perfectly fine. Run it 10 times, look at the distribution, and note the time of day with its time zone.

Step 2: measuring “the data arrived but the connection never finished”

If the table points at the third row, you need finer observation: read the response chunk by chunk, recording each chunk’s arrival time and the gaps between them. The question to answer is — once the last byte arrived, how long did the connection keep spinning?
Detection threshold: a tail_99 above 30 seconds, or a max_gap above 30 seconds, counts as one “tail stall”. The signature is a max_gap occurring at the point where the byte count already reached 100% — every byte arrived, and only then did the waiting begin.
Do not cover three phases with one timeout value.This is the easiest trap: using a single timeout for “waiting for the first byte” and “waiting for transfer” collapses two problems with completely different root causes into one indistinguishable failure.Split them and your logs will tell you directly whether it was “slow generation” or “delivered but never finished” — no guessing.

Step 3: benchmarking throughput between two servers

Between two machines you own

Use iperf3 to measure real throughput — this is the most accurate option:

From your server to our API

iperf3 cannot be used for this leg — we do not run an iperf server. Measure the effective rate from real calls instead:
Pair it with link-quality checks:
If the reading table points at “the finish signal never came”, mtr and ping are useless here. In that case not a single byte was lost and link quality is fine, so these tools will show nothing wrong — and you will have chased the wrong lead. Confirm which class you are in with the script above before investigating the network.

Check whether your bandwidth is enough

An image response body is one solid block of base64. Measured magnitudes: Base64 encoding itself inflates the payload by roughly 33%. Download time with the link to yourself: The catch is that this table assumes you have the link to yourself. In practice:
For example: a 10 Mbps egress, 30 concurrent image requests, 2.6 MB per response — each request gets roughly 0.04 MB/s, so the download alone takes 62 seconds, and not one of those seconds appears in the console log. Double the concurrency and that number doubles too.
This is why “it times out during the busy daytime but the same code is fine at night”. The model did not get slower; your bandwidth is being divided among more requests.

Step 4: which fields your instrumentation should record

To describe the symptom precisely — whether for your own analysis or to send to us — record at least these per call: That last column is the one people skip, yet it is often the conclusion itself: plot rate against concurrency, and if the rate falls proportionally as concurrency rises, bandwidth is your bottleneck and there is nothing else to look for. How to use the table: put it side by side with duration_for_view from the console log —
  • Close to each other → the problem is downstream transfer; look at bandwidth and concurrency;
  • Far apart → the problem is the finish signal or the client side.

Four things that reduce the risk immediately

1

Switch to URL output — the highest-leverage change

gpt-image-2-vip and gpt-image-2-all accept response_format: "url", returning an image link instead of base64. The response body drops from about 2.6 MB to about 0.3 KB — which removes the download and finish-signal problems at the same time, because a small response carries Content-Length and the client knows on its own when it is done.If your business depends on URL output, switch the token’s group to image2_OSS: deterministic URL output that will not degrade to base64 under resource pressure, at a 1x multiplier with no markup.
The official-relay gpt-image-2 does not support this parameter and returns 400 unknown_parameter if you send it. Base64 is currently its only output path.
2

Shrink the response body

When you must stay on base64: output_format=jpeg with output_compression cuts the size by more than half versus PNG; lower size and quality to what you actually need instead of defaulting to 4K. Compressing input reference images to under 1.5MB helps the upload leg too.
3

Cap concurrency at what your bandwidth supports

Invert the formula above: acceptable download time × egress bandwidth ÷ per-image size is your concurrency ceiling. Past it, more concurrency only makes every request slower without raising total throughput. Per-model limits are in How much concurrency can I use?.
4

Split the timeout into three, and finish proactively once data is complete

Set the first-byte, inter-chunk, and finish-grace timeouts separately as described above. When the data is complete but the finish signal never arrives, hand the response you already hold to your application — full compatibility code is in Image request finish stall.

Common questions

Because billing happens when the gateway finishes processing, and by then upstream really had generated and returned the result. Whether your client goes on to receive it does not change the cost already incurred. Measured comparison: a client that disconnects at 5 seconds is billed exactly the same as one that runs to completion.Turn it around and this is the strongest diagnostic you have: a billing record means the request genuinely reached upstream and succeeded, so the problem must lie after the gateway finished processing, or before the request was truly sent — no need to suspect upstream.Image endpoints have no async task ID, so disconnecting loses the result; see Is there an async image API?.
It depends — which is exactly why you measure before changing anything:
  • Slow download (low rate, byte count still climbing): yes, raising it gets you the result.
  • Finish signal never came (bytes complete long ago, nothing new at the end): no. We measured 330 seconds of continued waiting in this state without a single new byte; a longer timeout only delays the moment you notice. Here you need to finish proactively on the client.
It can be either, which is why you measure first. Here are the criteria for both sides:
  • Points at you: a clearly low speed_download, a rate that falls as concurrency rises, packet loss in mtr, or curl behaving fine while only your application code times out.
  • Points at us: bytes complete long ago with a long stretch of zero new data at the end. The gateway did have a “delayed finish signal” problem — its root cause was billing accounting on the image path blocking request handling — fixed and verified with an upstream release on 13 August 2026. Even in completely healthy periods, roughly 4% of requests still wait 10–79 extra seconds for the finish signal, with their data already fully delivered.
Send us the per-segment measurements once you have them; it is far more actionable than “it’s slow”. The fields to include are in the next section.
Image generation is currently synchronous across the board, with no task-ID lookup endpoint. An asynchronous option is on the roadmap and will be announced separately when it ships.Until then we recommend wrapping an async shell on your own side (return a local task ID on submission, let background workers make the synchronous call); see Building your own async queue.
Depends which class you are in. Insufficient bandwidth is a property of your egress, so changing our entry address does nothing — you need more bandwidth, smaller payloads, or lower concurrency. The finish-signal class appeared at multiple entry points simultaneously inside a window and recovered simultaneously, so changing domains does not route around it either.The one address to avoid is the CDN node: api-cf.apiyi.com runs through Cloudflare and returns 524 at roughly 100 seconds, which is unsuitable for long image requests.

Three easily missed client-side causes

If curl measures clean and only your application code times out, look here:
  1. The timeout does not mean what you think. Is your 600 seconds a total timeout or only a read timeout? Node’s undici has three independent timeouts — headersTimeout, bodyTimeout, and connect.timeout — whose defaults are far below whatever you set on the outer layer, and changing only the outer one has no effect.
  2. Connection-pool queueing. When the pool is saturated, the clock starts before the request is actually sent. That wait is completely invisible to us — there is no record of the request in the backend log until it truly goes out. Diagnostic: no matching log record generally means this class.
  3. There is another layer in between. A self-hosted nginx defaults proxy_read_timeout to 60 seconds, and load balancers, API gateways, and serverless platforms each impose their own ceilings. List the timeout at every hop; the smallest one is your real timeout.

What to include when reporting

If you still need our help after the self-check, send these together to save several rounds:
  • Request IDs (a handful is enough, not the full set)
  • Per-segment timings: time to first byte / last-byte time / total time / response body bytes
  • When it happened, with the time zone (e.g. 2026-08-13 15:57 (UTC+8))
  • Concurrency at the time and your egress bandwidth
  • Which model and token group you were using

How do I avoid API timeouts?

Recommended timeouts per scenario, and endpoint selection

Image request finish stall

Client-side handling when data is complete but the connection will not end

Image API connection drops

Diagnosing ECONNRESET and SSL EOF style downstream disconnects

How much concurrency can I use?

Per-model concurrency limits and quota requests

Image API best practices

Per-model timeout cheat sheet and output format comparison

Reading the billing amount in your logs

What each console log column means and how billing is recorded

Contact us

WeCom Support

WeCom support QR codeScan the QR code or contact supportImage timeouts and slow-download diagnosis

Email