在介绍命令之前先做个简要说明:我们是 Geonode,主要销售代理服务,因此老实说,curl 超时几乎从来都不是代理的问题。 如果您的请求因服务器本身运行缓慢、主机离线,或是防火墙无声丢包而超时,那么通过我们的代理进行转发除了增加费用外,其他方面并不会有任何改变。 当问题在于请求看似来自哪里时,代理才派得上用场——例如受地理限制的内容、基于您IP地址的速率限制,或是被封禁的IP地址。它们无法让慢服务器变快。请先正确设置超时时间;您可能会发现其实根本不需要其他措施。
没错。这里有一个大多数人都是通过惨痛教训才发现的问题:curl 没有默认的总体超时设置。它的默认连接超时时间为 300 秒,但一旦建立连接,curl 就会无休止地等待一个永远无法完全收到的响应。 在 cron 中运行的 shell 脚本中,这会导致任务永远无法完成,且锁定文件无人清除。
两个关键的超时参数
其他所有设置都是对这两个参数的细化。
--max-time
(简写形式 -m
)是整个操作的上限。根据 curl 手册 的说明:“允许传输操作耗费的最大时间(以秒为单位)。”这包括连接、握手、请求、响应等所有环节。 当达到限制时,curl 会中止操作并以代码 28 退出。
curl -m 10 https://example.com
总计 10 秒,之后无论处于哪个阶段,curl 都会放弃。
--connect-timeout
仅对建立阶段设置限制。手册中明确说明了该阶段包含的内容: “当 DNS 解析以及请求的 TCP、TLS 或 QUIC 握手完成时,连接阶段即被视为完成。”一旦连接建立,此选项即不再适用。
curl --connect-timeout 3 https://example.com
三秒钟用于解析域名并完成握手。此后,curl 将等待直至传输完成。
实际上,您需要同时使用这两种选项,它们分别针对不同的问题:
| 选项 | 涵盖范围 | 典型值 | 可防范的问题 |
|---|
--connect-timeout | | | |
| DNS + TCP/TLS/QUIC 握手 | 3–10 秒 | 主机不可用、数据包丢失、DNS 故障 |
| --max-time
| 整个操作过程 | 10–60 秒,取决于工作负载 | 响应缓慢、传输停滞、数据流无休止 |
综合来看:
curl --connect-timeout 5 -m 30 https://example.com
建立连接需五秒,整个过程需三十秒。如果主机不可达,则五秒内就会失败,而非三十秒——当遍历包含一千个 URL 的列表时,这一点至关重要。
选择并非凭空猜测的数值
通常的做法是选一个整数,并在出现错误时将其向上调整。这样会逐渐趋向于一个足够大的数值,导致超时设置不再起任何实际作用。
有一种更好的方法,只需多执行一条命令。curl 可以报告时间实际消耗在哪些环节:
curl -o /dev/null -s -w "dns: %{time_namelookup}\nconnect: %{time_connect}\ntls: %{time_appconnect}\nttfb: %{time_starttransfer}\ntotal: %{time_total}\n" https://example.com
对实际目标运行该命令二十到三十次,你便能获得一个分布值,而非单纯的猜测。然后:
根据 time_appconnect 中的数据,设置 --connect-timeout。 取 p95 值并将其大致翻倍。连接时间主要取决于网络往返时间,且相对稳定;如果耗时是平时的两倍,则说明确实存在问题,而非仅仅是速度变慢。
从 time_total 设置 --max-time。 这里的倍数应更宽松——p95 的三到五倍——因为总时间取决于响应大小和服务器负载,这两者都会出现合理波动。过紧的 --max-time 会导致脚本不稳定,在普通的不顺日子里就会失败。
有两项针对特定工作负载的调整。如果您正在下载大文件,--max-time 完全不适合,因为正常的下载速度可能会超过任何合理的固定限制——请改用下文提到的基于速度的选项。 此外,若您调用的 API 会在服务器端执行实际运算,请先确认该服务的超时时间,并将您的超时时间设置得略高一些;如果对一个 12 秒返回的服务设置 10 秒超时,就意味着您既支付了运算成本,却又丢弃了结果。
分数秒和毫秒精度
这两个选项均支持小数,无论您的区域设置如何,都使用点作为分隔符。此功能自 curl 7.32.0 起已支持。
curl --connect-timeout 0.5 -m 2.5 https://example.com
连接耗时半秒,总计两秒半。适用于健康检查以及任何因每次失败需等待整整一秒而导致累计时间较长的循环场景。
有一点需要注意:随着数值的增大,精度会下降。curl 文档指出,随着指定超时时间的十进制精度增加,实际超时时间的精度会降低。编写 --max-time 30.001 与 --max-time 30 实质上并无区别。小数适用于小于一秒的值;超过几秒时,请使用整数。
相关的 --expect100-timeout 参数也支持小数。它控制 curl 在发送请求主体前等待 100 Continue 的时长,默认值为一秒。如果你向从不返回 100 Continue 的服务器发送大体积 POST 请求,那么每次请求都会白白浪费这一秒——要么降低该值,要么通过 -H "Expect:" 禁用此预期。
捕获不会超时的卡顿传输
问题就在这里。每隔几秒传输一个字节的传输永远不会处于空闲状态,因此不会触发连接级超时;而且,如果--max-time
的阈值设置得足够高(以适应正常的大型下载),该超时也不会触发。 请求只是缓慢地进行着。
``--speed-limit`
和 ``--speed-time
可以处理这种情况。根据手册说明:“如果在 ``--speed-time
期间,下载速度低于每秒--speed-limit`
字节,则传输将被中止。”
curl --speed-limit 1000 --speed-time 30 -O https://example.com/large-file.zip
如果吞吐量连续 30 秒保持在每秒 1000 字节以下,curl 就会中止下载——同样返回退出代码 28。一个以正常速度运行六小时的下载任务不会受到影响,而一个陷入停滞的下载任务则会在 30 秒内被终止。
对于任何大小不可预测的任务,这是正确的超时设置,且能很好地与其他机制配合:
curl --connect-timeout 5 --speed-limit 1000 --speed-time 30 -O https://example.com/large-file.zip
针对已死主机快速失败,不设总体上限,但全程配备停滞检测机制。就文件下载而言,这种模式应被纳入每个脚本——有关相关选项的详细说明,请参阅我们的指南 使用 curl 下载文件。
默认情况下,超时和重试机制存在严重冲突
这正是容易让人中招的地方。
--retry N
会让 curl 对瞬时错误重试最多 N 次。容易被忽略的是,--max-time
适用于每次尝试,而非整个命令,而且 curl 会在每次尝试之间采用指数退避策略,起始等待时间为 1 秒,随后每倍增一次。
因此,以下操作:
curl -m 10 --retry 5 https://example.com
确实可能耗时远超一分钟:五次尝试,每次最多十秒,再加上 1、2、4、8 和 16 秒的退避等待时间。如果你写这段代码时预期上限是十秒,那你就低估了六倍。
--retry-max-time
是解决方法。它限制了重试的总耗时:
curl -m 10 --retry 5 --retry-max-time 40 https://example.com
现在,一旦超过 40 秒,curl 就会停止发起新的重试。请注意这里的表述——超过限制后它不会启动新的重试,但已经开始的尝试会运行到其自身的 --max-time
。真正的最坏情况是 --retry-max-time
加上一个 --max-time
。
--retry-delay
将指数退避替换为固定等待时间,这使得总运行时间可预测:
curl -m 10 --retry 3 --retry-delay 2 --retry-max-time 40 https://example.com
还有一个值得了解的标志:默认情况下,--retry
仅在少数几种瞬态条件下触发。--retry-all-errors
则大大扩展了其适用范围——手册中将其描述为重试“所有瞬态错误,包括 FTP 4xx 和 5xx 响应码”。 在网络不稳定的脚本中,该功能确实非常有用;但在非幂等 POST 请求之前使用时,则确实非常危险。添加该功能前请三思。
通过代理时出现的超时问题
添加 -x
后,计时情况会发生变化,因为此时连接不再是单一的,而是变成了两条:你与代理之间的连接,以及代理与目标之间的连接。
curl -x http://user:pass@proxy.example.com:9000 --connect-timeout 10 -m 45 https://example.com
有三点与直接连接的情况不同。
**--connect-timeout
测量的是你到代理的跳数,而不是代理到目标的跳数。** 对于 HTTPS,curl 会发出 CONNECT
请求,由代理建立后续连接;该过程所耗时间计入 --max-time
,而非 --connect-timeout
。因此,即使设置了较短的连接超时时间,也无法防范那种虽然迅速接受你的连接,但随后却需要二十秒才能到达目标的代理。
住宅代理的速度确实较慢,这是正常现象。 流量是通过真实的用户连接传出的,因此多花几百毫秒是正常现象,而非故障。针对直接连接调优的超时值会导致失败,这些失败看似是代理故障,实则只是物理限制所致。请使用上述 -w
命令通过代理进行测量,并根据该测量结果设置超时值,而非基于直接连接的数值。
**失败原因往往难以确定。**通过代理返回的退出代码 28 可能意味着代理速度慢、目标服务器速度慢,或者目标服务器正在故意拖延您的请求。要区分这些情况,需要分别测试各个组件,这本身是一个相当庞大的课题,因此我们专门撰写了关于测试代理的指南。
针对代理请求的实用模式:设置宽松的--connect-timeout
(10秒),根据实际测量结果(而非主观臆测)设定--max-time
,并且不要使用--retry-all-errors
,因为通过代理时,5xx错误通常意味着目标服务器正在拒绝你的请求,而非暂时性故障,而重试只会使情况恶化。
读取退出代码
curl 的退出代码会告诉你哪个阶段失败了,这些信息比大多数脚本所关注的要详细得多。
| 代码 | 名称 | 含义 |
|---|
| 6 | CURLE_COULDNT_RESOLVE_HOST | DNS 失败 — 未超时 |
| 7 | CURLE_COULDNT_CONNECT | “无法通过 connect() 连接到主机或代理” |
| 28 | CURLE_OPERATION_TIMEDOUT | “已达到指定的超时时间” |
| 56 | CURLE_RECV_ERROR | 接收网络数据失败 — 传输中途连接中断 |
上述描述摘自 libcurl 错误参考。
代码 7 和 28 之间的区别尤为重要。代码 7 表示连接被主动拒绝——即对方迅速响应并拒绝了请求;代码 28 表示未及时收到响应。前者通常表明端口错误或服务已关闭;后者则表明数据包丢失、防火墙无声过滤,或是主机确实处于过载状态。 对于代码 28,重试是合理的;而对于代码 7,重试通常毫无意义。
在脚本中:
curl --connect-timeout 5 -m 30 -sS https://example.com > out.txt
case $? in
0) echo "ok" ;;
6) echo "dns failure" ;;
7) echo "connection refused" ;;
28) echo "timed out" ;;
*) echo "other failure" ;;
esac
请注意,这三种超时机制——--max-time、--connect-timeout 以及速率限制对——都会返回代码 28。退出代码仅表明已达到某个限制,但不会指出具体是哪一项。若需确认,请使用 -w "%{time_total}" 并将其与您配置的值进行对比。
何时不应设置超时
尽管普遍建议如此,但在某些情况下,超时并非合适的解决方案。
交互式下载。 如果你正在终端中输入 curl 命令来下载一个大文件,那么你自己就是超时机制。你可以看到进度条,并按下 Ctrl-C。 在此处添加 -m 只会带来下载在 90% 时中断的烦扰。
长时流。 服务器发送的事件、日志尾部监视、按设计保持打开状态的分块响应——--max-time 会在最不恰当的时刻终止这些操作。如果你需要停滞检测器,请使用 --speed-limit 和 --speed-time,或者干脆不使用任何设置。
**任何已被外部限制包裹的情况。**如果 curl 在 timeout(1) 环境下运行,或处于配置了 RuntimeMaxSec 的 systemd 单元中,或处于拥有独立配额的 CI 步骤中,增加第二层限制只会徒增一个需要同步的数字。请选择能生成您希望看到的错误消息的那一层,并在该处进行设置。
作为目标服务器响应缓慢的解决方案。 超时只会让缓慢的请求更快失败,并不能使其成功。如果你的实际问题是服务器响应需要 40 秒,可选方案包括缓存、 分页、使用其他端点,或是与服务器管理员沟通——在此我们需重申开头的免责声明,因为这正是人们购买代理最常试图解决的问题,却也是代理完全无法解决的少数问题之一。 我们的定价截至2026年9月,住宅流量起价为0.79美元/GB,数据中心流量起价为0.14美元/GB,但这些服务均无法加快响应缓慢的源服务器的速度。
大家还问
curl 的默认超时时间是多少?
curl 没有默认的总体超时时间——一旦建立连接,curl 会无限期等待响应。但连接阶段的默认超时时间为 300 秒。这就是为什么在脚本中设置 -m 很重要:如果不设置,请求卡住会导致脚本卡死。
--max-time 和 --connect-timeout 有什么区别?
--connect-timeout 仅适用于 DNS 解析和 TCP/TLS/QUIC 握手过程,一旦连接建立,该超时设置即不再生效。--max-time 则涵盖从开始到结束的整个操作过程,包括响应传输。 建议同时使用这两项参数——设置较短的连接超时以快速识别死主机,同时设置较长的最大时间作为整体上限。
为什么我的 curl 命令耗时超过了设定的超时时间?
几乎总是因为重试。--max-time 针对每次尝试生效,而 --retry 既增加了额外尝试次数,又在尝试之间引入了指数退避机制。添加 --retry-max-time 可限制总次数,并请记住最坏情况是该值加上一次 --max-time。
如何以毫秒为单位设置 curl 超时?
使用小数值:--connect-timeout 0.25 表示 250 毫秒。--max-time 和 --connect-timeout 均支持以点分隔的小数值,该功能自 curl 7.32.0 起提供。精度最好控制在几秒以内;对于更大的数值,小数部分的精度会下降。
curl 在超时的情况下会返回什么退出代码?
28,CURLE_OPERATION_TIMEDOUT。这三种超时机制都会返回该代码,因此该代码仅表示已达到某个限制,但不会具体指出是哪一种。请将其与 7(连接被拒绝)和 6(DNS 失败)进行区分,后两者表示不同的情况,通常不应重试。
如何对下载速度缓慢的情况设置超时,同时避免中断大文件下载?
请使用 --speed-limit 和 --speed-time 代替 --max-time。这些命令仅在吞吐量持续低于阈值一段时间后才会中止下载,因此正常的数小时下载不会受影响,而卡住的下载则会迅速终止。
通过代理时,超时机制是否不同?
是的。--connect-timeout 仅测量您与代理之间的连接;代理与目标服务器之间的后续连接则由 --max-time 负责。家庭代理还会增加实际延迟,因此针对直接连接调优的数值会导致错误的失败判定。请通过代理进行测量,并据此设置数值。
我可以单独为 DNS 解析设置超时吗?
没有单独的选项——DNS 已包含在 --connect-timeout 中。如果您需要专门限制 DNS,请单独进行解析,并通过 --resolve 传递结果,这将完全跳过 curl 自身的查询。
总结
有两种选项几乎可以覆盖所有情况:当主机不可达时,使用 --connect-timeout 实现快速失败;使用 --max-time 设置总体时间上限。请务必在每个脚本中都设置这两项。默认的“无限等待”对于交互式工具来说是个合理的选择,但对于自动化脚本来说却是个糟糕的选择。
有两点容易让人犯错,值得再次强调。--max-time 是按每次尝试计算的,因此任何使用 --retry 的情况都需要同时配合 --retry-max-time,否则原本 10 秒的命令就会变成 1 分钟的命令。此外,对于大小不可预测的传输,固定时间限制并非合适的解决方案——将 --speed-limit 与 --speed-time 结合使用,即可获得一个停滞检测器,它不会干扰正常且耗时的下载。
请根据实际测量结果设置参数,而非凭感觉选择一个“看起来合适”的整数值。对实际目标运行一次 curl -w 即可获得数据分布,而基于该分布推导出的超时设置,只会在出现真正问题时触发失败,而非在网络遭遇普通波动时就贸然中断。