Gemini API 超时排查:ReadTimeout 与 504 的正确处理方式

超时报错的原文有五种,先认脸

客户端自己抛的超时

服务端什么都没返回,卡住的是你这一端的等待逻辑,典型原文是这几条:

关键字是 ReadTimeouttimed out,后面括号里的数字就是你自己设的超时值。看到 60 秒这个数字,基本能确定是默认值没改。

网关或服务端返回的 504

{"error": {"code": 504, "message": "Deadline exceeded", "status": "DEADLINE_EXCEEDED"}}

Deadline exceeded 说明请求已经送到末端一跳,是处理时间超过了限定的截止时间。它和客户端超时不是一回事:前者要放宽的是服务端与网关的时间预算,后者要改的是你代码里那个数字。

半路断掉的流

开了流式之后,常见的是先在终端看到几十个 chunk,然后突然报 Connection reset by peer 或者 RemoteProtocolError: peer closed connection without sending complete message body。这类多半是中间层的缓冲或超时把长连接掐了,而不是模型本身挂了。

为什么 Gemini 类模型特别容易超时

推理型模型会先想再答

gemini-3-pro-preview、gemini-3.1-pro-preview 这类带推理的模型,收到问题后先在内部推演,再吐出首字。一道复杂的分析题,内部推理几分钟属于正常现象。

非流式请求的等待时间等于全部处理时间

不开流式时,客户端从发请求到收到响应之间一直是静默的,中间没有任何数据回来,正好命中 read timeout。把默认的 60 秒用在推理模型上,几乎必然超时。

长上下文会拉长首字节时间

塞进去几万字的文档做总结,服务端要先读完再答,首字节时间自然被拉长。这跟输入长度不影响单次价格不冲突——计费按次数算,但处理时间确实随输入变长。

超时参数该怎么设

三个超时要分开管

连接超时管的是握手,读取超时管的是等响应,总时长管的是整次请求。三个混成一个,就会出现连接一直没有响应的请求把线程占很久的情况。经验值参考:

参数建议值说明
连接超时10 秒握手失败要立刻换路,别耗着
读取超时(非流式)300 秒推理模型要留足思考时间
读取超时(流式)60 秒每两个 chunk 之间不该等 60 秒
单请求总时长600 秒兜底,防止极端情况挂死
重试次数上限2 次按次计费,重试是重复花钱

按任务类型分别设值

短问答给 60 秒足够,代码生成给 180 秒,长文档总结和深度分析给 300 到 600 秒。全项目用一个超时值,要么短任务被拖慢,要么长任务被误杀。

别把超时设成不限制

设成 0 或者不限制,一次网络抖动就能把工作线程长期占住,几十个这样的请求就足以让整个服务排队卡死。宁可让它失败重试,也不要让它一直等下去。

给每个请求加上总时长兜底还有另一个好处:长尾请求不会一直堆在工作队列里。超时后被及时释放,后面的任务才能按时开始,整体吞吐反而更高。

超时值也不建议一步拉到极限。先给一个保守值跑一周,统计真实耗时分布,再按 P99 往上留三成余量,比一开始就设成 900 秒更合理,也更容易发现真正的慢任务。

Python 里的正确写法

OpenAI 兼容入口分开设置三段超时

import httpx
from openai import OpenAI

client = OpenAI(
    api_key="你的平台KEY",
    base_url="https://xyuapi.top/v1",
    max_retries=2,
    timeout=httpx.Timeout(connect=10.0, read=300.0, write=30.0, pool=10.0),
)

resp = client.chat.completions.create(
    model="gemini-3-pro-preview",
    messages=[{"role": "user", "content": "把这份合同拆成条款清单"}],
)
print(resp.choices[0].message.content)

httpx.Timeout 支持分段传值,这一段比一个孤零零的数字好用得多。

请求级别可以覆盖全局设置

长任务调用时单独传一次更大的超时,其余调用仍然维持短超时,避免为了个别慢任务把全局阈值抬高:

resp = client.chat.completions.create(
    model="gemini-3.1-pro-preview",
    messages=[{"role": "user", "content": long_text}],
    timeout=600,
)

捕获超时要单独处理

from openai import APITimeoutError, APIConnectionError

try:
    resp = client.chat.completions.create(model="gemini-2.5-pro", messages=msgs)
except APITimeoutError:
    # 超时通常可以安全重试:这类请求是只读的
    resp = client.chat.completions.create(model="gemini-2.5-pro", messages=msgs, timeout=600)
except APIConnectionError as e:
    print("连接层问题:", e)

把超时和连接错误分开处理,能避免所有网络异常都被塞进同一个重试分支里反复重试。

开流式是见效比较快的止血手段

流式让首字节时间降到几秒

开了流式,服务端每产出一段就发一段,客户端不会一直静默等待,超时阈值可以从 300 秒降到几十秒,用户体验也从「转圈两分钟」变成「字一个个往外冒」。

stream = client.chat.completions.create(
    model="gemini-3-pro-preview",
    messages=[{"role": "user", "content": "写一份周报模板"}],
    stream=True,
    timeout=60,
)
buf = []
for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        buf.append(delta)
        print(delta, end="", flush=True)
print("\n总字数:", len("".join(buf)))

流式要处理「流到一半断掉」

中断后不能续传,只能记录已收到的内容,然后决定是重发一次还是直接采用半成品。判断依据看已经收到的内容有没有完成主体结构。

流式下不要做长耗时处理

打印和写文件都要轻,别在循环里做大数据量运算,否则会把接收缓冲拖满,反而触发断流。

还有一点:流式断连后不要悄悄吞掉。至少记录断在第几个分片、已经收到多少字、当时耗时多少。这些数据攒起来就能看出是固定时长断还是固定字数断,两种情况的处理方向完全不同。

同一台机器上多个任务同时开流式时,注意连接池大小。默认池子偏小时,新请求会排队等连接,表现成「明明没超时却卡住」,把池子调大或者限制并发数都能解决。

网关层的超时链

nginx 默认 60 秒会掐断长请求

自建反代时,proxy_read_timeout 默认 60 秒,模型还在思考,nginx 已经先返回 504 了。正确配置是把读取超时放宽,并关掉缓冲,让流式数据实时透传:

location /v1/ {
    proxy_pass http://127.0.0.1:5010;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
    proxy_buffering off;
    chunked_transfer_encoding on;
}

每一层都要单独放宽

浏览器到 CDN、CDN 到网关、网关到服务、服务到上游,这四段各有自己的超时。只改一层,其余层照旧掐断,表现就是「明明设了 600 秒还是 60 秒报错」。

容器和负载均衡也有默认值

容器编排平台的探针、云负载均衡的空闲超时同样会断开长连接。排查时把链路画出来,逐段核对自己的配置,比反复改客户端超时有效。

排查这类问题有个省事的办法:拿一个短请求和一个长请求各测一遍。短请求正常、长请求固定在第 N 秒断开,那个 N 基本就是某一层的超时值,照着这个数字去各层配置里搜,很快就能找到对应的那一行。

如果链路里还有 CDN,注意它的空闲连接回收往往比网关更激进。开流式并保持每几秒有数据往返,是稳妥的保活方式。

改完配置记得验证生效:把长请求重跑一次,看能不能稳定越过原来的断点。只确认文件改过了,不算验证。

重试与成本控制

超时请求能不能重试

对话类请求是只读操作,不会产生副作用,超时后重发是安全的。但要控制次数:按次计费的模式下,每次重试都是一次实打实的调用。

重试要带抖动,还要和限流配合

一批请求同时超时、同时重发,很容易接着撞配额上限。重试间隔加上随机抖动,并且重试也走同一个并发信号量,两个问题一起解决。

把钱花在刀刃上

模型价格处理时长预期建议超时
gemini-2.5-pro0.031 元/次5 到 30 秒120 秒
gemini-3-pro-preview0.05 元/次10 到 90 秒300 秒
gemini-3.1-pro-preview0.09 元/次30 到 240 秒600 秒
claude-sonnet-4-5-thinking0.09 元/次20 到 180 秒300 秒
NanoBanana-Pro(生图)0.27 元/次10 到 60 秒180 秒

按任务实际需要的模型选型,比统一用贵模型更划算:能一次搞定的简单抽取给 gemini-2.5-pro,几百条数据跑下来成本差得很明显。

和其他故障区分开

一张对照表省下反复试错

现象报错关键字根因位置处理方向
等满 60 秒报错ReadTimeout客户端超时值按任务分别放宽阈值
约 60 秒收到 504Deadline exceeded网关或反代放宽 proxy_read_timeout
流到一半断Connection reset中间层缓冲或超时关缓冲、缩短空闲
一直没响应也不报错DNS 或连接被丢弃加连接超时并换线路
偶发超时、重试即过APITimeoutError上游瞬时负载有限重试加抖动

上游过载不等于你的配置有问题

高峰期偶发的超时是上游瞬时负载造成的,重试一次通常就过。只有当超时集中在固定时段、固定任务类型上,才说明配置需要调。

排查顺序从下往上

curl -w 看各阶段耗时,再确认网关日志里有没有对应记录,之后才怀疑客户端代码。反过来查,往往在代码里来回改半天,问题其实在 nginx 那行默认值上。

一次抓全的排障手法

用 curl 把各阶段耗时打出来

curl -s -o /dev/null -N -w 'dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} first:%{time_starttransfer} total:%{time_total} code:%{http_code}\n' \
  --connect-timeout 10 --max-time 300 \
  https://xyuapi.top/v1/chat/completions \
  -H "Authorization: Bearer $XYU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gemini-2.5-pro","messages":[{"role":"user","content":"ping"}]}'

time_starttransfer 明显偏大而 time_connect 很小,说明慢在处理阶段,要放宽的正是读取超时。

日志里把关键字段一起打

每次调用记录模型名、输入长度、耗时、是否超时、重试次数。四个字段凑齐之后,一眼就能看出是哪个模型、哪类输入在拖时间。

留一份慢样本

把连续两次超时的输入单独存下来,作为回归用例。改完参数复跑一遍,确保没有把别的任务拖慢。

聚合入口下的超时表现

接口信息

主入口 https://xyuapi.top/v1,备用 https://xyuai.cc/v1,另支持 Gemini 原生 /v1beta 路径,OpenAI 兼容协议,Chatbox、Cherry Studio、NextChat、Cline 这类客户端填上地址和 key 即可使用。

按次计费与超时的关系

一次请求固定价,输入多长都不影响单价。超时重试会重复计费,所以正确做法是先把超时阈值按任务类型设对,把重试次数压到两次以内,而不是放任重试。

现在的动作清单

  1. 把全局超时从默认值改成按任务分档,非流式长任务给 300 秒以上。
  2. 长任务一律改成流式调用,让首字节时间回到几秒。
  3. 自建反代的话,把读取超时放宽到 600 秒并关掉缓冲,逐层核对。
  4. 给超时单独写一个重试分支,带抖动、带上限、记日志。

相关阅读

🚀 想要立即使用?来小鱼API体验全系列AI模型

查看全部产品

支持Gemini / Claude / GPT / Grok / DeepSeek · 国内直连 · 支付宝/微信