bigywater.tech
爬虫工程 · 5

超时的几种含义:requests 的 timeout 为什么不是总时长

“我给 requests 设了 timeout=5,为什么还是卡了一分钟?”因为 timeout 不是总时长。这篇用本机服务器把连接超时、读超时、慢速滴灌的响应、以及不设 timeout 会怎样都复现一遍。

3023 ms
timeout=1.0 却成功:服务器每 0.5 秒发 1 个字节,整个响应用了 3 秒
ConnectionError
读响应体阶段超时抛的异常,不是 ReadTimeout
True
不传 timeout,3.0 秒后请求仍在阻塞

连接超时

实测 · 本机执行输出
  (本机回环做不出连接超时:macOS 上监听队列满了之后,新连接是立刻被重置,而不是卡住)
  timeout=(0.5, 5) → ConnectTimeout,耗时 503 ms
  timeout=(1.5, 5) → ConnectTimeout,耗时 1502 ms
  timeout=1.0      → ConnectTimeout,耗时 1002 ms
  异常类的继承关系:ConnectTimeout → ConnectionError → Timeout → RequestException → OSError
  ReadTimeout 的继承关系:ReadTimeout → Timeout → RequestException

连一个没人回应的地址,timeout=(0.5, 5) 在 503 ms 抛 ConnectTimeout,耗时就是设定值。ConnectTimeout 继承自 ConnectionError,所以 except ConnectionError 能抓到它;ReadTimeout 不在这条继承链上。

这里用了 192.0.2.1(RFC 5737 保留给文档的地址,不属于任何真实站点)。本机回环做不出连接超时——macOS 上监听队列满了,新连接会被立刻重置而不是卡住。

读超时

实测 · 本机执行输出
  timeout=(5, 1.0)   → ReadTimeout,耗时 1002 ms
  timeout=1.0        → ReadTimeout,耗时 1002 ms
  timeout=(0.2, 3.0) → 成功 200,耗时 2003 ms

建立连接之后,timeout 的第二个数管“发出请求后等对方数据”。服务器 2 秒后才回应,(5, 1.0) 在约 1 秒时抛 ReadTimeout;(0.2, 3.0) 的连接超时虽然很短,但本机连接瞬间成功,不受影响。

不是总时长

实测 · 本机执行输出
  timeout=1.0(每个间隔 0.5 秒)→ 成功 200,收到 6 个字节,耗时 3023 ms
  timeout=0.3(每个间隔 0.5 秒)→ ConnectionError:HTTPConnectionPool(host='127.0.0.1', port=9960): Read timed out.,耗时 302 ms
  requests/models.py 第 942-943 行:except ReadTimeoutError as e: → raise ConnectionError(e)

读超时限制的是每次等待数据的时间。服务器每 0.5 秒发一个字节,timeout=1.0 下整个 3 秒的响应照样成功。读响应体阶段超时的异常也不同:requests 在 models.py 里把 ReadTimeoutError 转成了 ConnectionError,所以只捕获 ReadTimeout 会漏掉这种情况。

自己做总时长上限

实测 · 本机执行输出
  服务器每 0.2 秒发 1 个字节永不结束,timeout=1.0 + 自己的 2 秒截止时间:超过自己设的 2 秒截止时间,主动停止;耗时 2044 ms,收到 10 个字节

用 stream=True 配合 iter_content,在循环里检查截止时间,到点主动停止并关闭响应。实测滴灌的响应 2.044 秒后被截断,收到 10 个字节。这只是最简单的做法;我没有测同时限制连接阶段和响应头阶段的总时长。

不传 timeout

实测 · 本机执行输出
  requests.request 文档里对 timeout 的说明::param timeout: (optional) How many seconds to wait for the server to send data before giving up, as a float, or a :ref:`(connect timeout, read timeout) <timeouts>` tuple.
  服务器收到请求后一直不回应,3.0 秒后这个没有 timeout 的请求还在阻塞吗?True(服务器已收到 1 个请求)
  放行之后它的结果:200,总耗时 3003 ms

默认是 None,也就是一直等。一个收到请求却不回应的服务器,可以让没有 timeout 的请求无限期阻塞。爬虫里每个请求都应显式传 timeout(连接和读取分开传)。

分步演示

requests 的 timeout
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。连接超时用 RFC 5737 的 TEST-NET-1 地址 192.0.2.1(只发出 SYN,没有回应);其余用本机服务器,分别延迟回应、每 0.5 秒滴灌 1 个字节、每 0.2 秒滴灌且永不结束、收到请求后一直不回应;源码位置取自 requests 的 models.py 与 api.py
    • 没有验证:DNS 解析阶段的耗时是否计入连接超时;TLS 握手阶段的超时归属;代理下的超时语义;aiohttp 的 ClientTimeout(total、connect、sock_read)(后面的文章会单独看);Windows 或 Linux 上监听队列满时的连接表现(只在 macOS 上看到立刻被重置)
    京ICP备2026067502号-1京公网安备11010502063487号