bigywater.tech
爬虫工程 · 31

429 与 Retry-After:服务端告诉你等多久

被限流的时候,服务器通常会用 429 加 Retry-After 告诉你该等多久。你的爬虫是听,还是立刻重试?这篇用一个固定窗口限流服务器,量一量听与不听的差别,并弄清 urllib3 到底怎么解析这个头、会重试哪些响应。

110 对 23
限流服务器下 20 个请求:立刻重试与遵守 Retry-After 时服务器收到的请求数
21600 秒
urllib3 对 Retry-After 的默认上限(6 小时),retry_after_max 可改
503 + Retry-After
即使 status_forcelist 里没有 503,也会被重试

怎么解析

实测 · 本机执行输出
  Retry-After: '5'                                → 5.0 秒
  Retry-After: ' 5 '                              → 5.0 秒
  Retry-After: '0'                                → 0.0 秒
  Retry-After: '-3'                               → InvalidHeader: Invalid Retry-After header: -3
  Retry-After: '1.5'                              → InvalidHeader: Invalid Retry-After header: 1.5
  Retry-After: '3600'                             → 3600.0 秒
  Retry-After: '86400'                            → 21600.0 秒
  Retry-After: 'Mon, 12 Oct 2026 02:49:59 GMT'    → 9.6 秒
  Retry-After: 'Mon, 12 Oct 2026 02:49:39 GMT'    → 0.0 秒
  Retry-After: 'soon'                             → InvalidHeader: Invalid Retry-After header: soon
  Retry-After: ''                                 → InvalidHeader: Invalid Retry-After header: 
  Retry.sleep_for_retry 的实现:
        def sleep_for_retry(self, response: BaseHTTPResponse) -> bool:
            retry_after = self.get_retry_after(response)
            if retry_after:
                time.sleep(retry_after)
                return True
    
            return False

urllib3 的 Retry.parse_retry_after 接受整数秒(允许前后空白)和 HTTP-date;小数、负数、乱写的值抛 InvalidHeader;日期早于现在是 0 秒;值超过 retry_after_max 就取上限(默认 21600 秒)。

听与不听

实测 · 本机执行输出
  客户端                                          成功    最终失败      总耗时     服务器收到   其中 429
  不重试(默认 max_retries=0)                       5      15    0.0 秒       20 个       15
  重试,忽略 Retry-After,固定 backoff 0(立刻重试)        5      15    0.0 秒      110 个      105
  重试,忽略 Retry-After,backoff_factor=0.2       20       0    2.8 秒       28 个        8
  重试,遵守 Retry-After(urllib3 默认)              20       0    3.0 秒       23 个        3

每个自然秒 5 个请求的限流下,20 个请求:不重试成功 5 个;重试但立刻重来,6 次重试在同一秒里用光,服务器收到 110 个请求却仍有 15 个失败;遵守 Retry-After 全部成功,服务器只多收到 3 个 429。即使忽略头,backoff_factor=0.2 的指数等待也成功了(28 个请求),但不如直接听服务器的。

日期写法

实测 · 本机执行输出
  12 个请求:成功 12,最终失败 0,总耗时 1.6 秒,服务器收到 14 个请求

Retry-After 也可以写成 HTTP-date,urllib3 同样遵守,换算成距现在的秒数。

上限

实测 · 本机执行输出
  Retry().parse_retry_after("86400") = 21600 秒;Retry(retry_after_max=60).parse_retry_after("3600") = 60 秒
  服务器回 429 + Retry-After: 3,retry_after_max=默认:重试后得到 200,总耗时 3.0 秒
  服务器回 429 + Retry-After: 3,retry_after_max=1:重试后得到 200,总耗时 1.0 秒

服务器能决定你的客户端睡多久。默认上限 6 小时,太长了:按能接受的最长等待设置 retry_after_max(演示里 3 秒的等待被压到 1 秒)。

哪些响应会被重试

实测 · 本机执行输出
  响应 503 + Retry-After: 1,status_forcelist=[500]:最终 200,服务器收到 2 个请求,耗时 1.0 秒
  响应 503(无 Retry-After),status_forcelist=[500]:最终 503,服务器收到 1 个请求,耗时 0.0 秒
  响应 429 + Retry-After: 1,status_forcelist=[]:最终 200,服务器收到 2 个请求,耗时 1.0 秒
  响应 500 + Retry-After: 1,status_forcelist=[]:最终 500,服务器收到 1 个请求,耗时 0.0 秒
  响应 503 + Retry-After: 1,status_forcelist=[503]:最终 200,服务器收到 2 个请求,耗时 1.0 秒
  响应 302 + Retry-After: 1,status_forcelist=[]:最终 302,服务器收到 1 个请求,耗时 0.0 秒

只要给 Retry 设了 total,带 Retry-After 的 413/429/503 就会被重试,不论是否在 status_forcelist 里;不带头的 503、带头的 500、302 都不会。要关掉就设 respect_retry_after_header=False(上面第二节用过)。

分步演示

429 与 Retry-After
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。限流服务器:每个自然秒最多接受 5 个请求,超出回 429 与 Retry-After(窗口剩余秒数向上取整,或 HTTP-date);客户端 requests 2.34.2 的 Session 挂 HTTPAdapter(max_retries=Retry(…)),urllib3 2.7.0;“服务器收到”取服务器日志的请求数;函数行为同时读了 urllib3 的 Retry.is_retry、parse_retry_after、sleep_for_retry 源码
    • 没有验证:aiohttp 或 httpx 的 Retry-After 处理(它们没有内置);Retry-After 与 X-RateLimit-Reset、RateLimit-Remaining 等其它限流头的配合;多个客户端共享同一个限流配额;重试期间连接是否保持;非幂等方法(POST)是否会被 Retry 重试(本系列后面有 POST 重试的专门一篇);真实 API 的 429 语义
    京ICP备2026067502号-1京公网安备11010502063487号