修复 429 请求过多:退避、预算和 IP 分散

429 是唯一一个告诉你如何修复的阻断——如果你阅读响应头而不仅仅是重试。以下是安全抓取吞吐量背后的算术原理。

HTTP 429 请求过多是网站可以发送给你的最诚实的阻断。与 403 不同,它指出了问题——你速度太快了——并且通常在响应头中提供了解决方案。然而,标准反应是将调用包装在重试循环中并希望成功,这将一个可解决的节奏问题转化为一个缓慢的、产生阻断的混乱。该指南将 429 视为它本质上是的:一个有四个杠杆的算术问题。阅读响应,按照公布的限制调整速度,正确退避当你超出时,并将剩余负载分散到多个身份。

429 的含义,以及何时它在撒谎

速率限制器根据身份计数请求——通常是 IP 地址,有时是 API 密钥或会话 cookie——在一个时间窗口内。超过阈值,你会得到 429 而不是内容。实现方式不同:令牌桶分配一个固定的额度,按计划补充,滑动窗口在滚动期间而不是时钟分钟内计数,分级系统在较高的硬阻断之前在软限制处进行节流。你面临的哪种情况决定了短暂停顿是否足够,还是必须等待整个窗口。

现在是节省时间的警告:在你的第一个请求上的 429 不是速率限制。这是一个穿着速率限制外衣的机器人响应。一个广为阅读的 Stack Overflow 线程正是描述了这一点——抓取器的第一个调用返回了一个页面,上面写着“内容抓取器行为不当 请使用 robots.txt 你的 IP 已被速率限制”,并附带 429。没有任何东西被超出;服务器只是决定客户端是一个机器人并选择了该代码。如果你在发送任何量之前看到 429,将其视为检测问题,并在我们的403 禁止错误指南中处理头和 IP 检查。

在更改任何代码之前阅读响应

行为良好的服务器会告诉你何时返回。Retry-After 包含秒数或 HTTP 日期。许多 API 添加 X-RateLimit-Limit(上限)、X-RateLimit-Remaining(当前窗口中剩余的)和 X-RateLimit-Reset(何时补充)。这三者将被动重试转化为主动节奏:你可以在阻断之前而不是之后放慢速度。

import requests

r = requests.get("https://target.example/api/items", timeout=20)
print(r.status_code)
for h in ("Retry-After", "X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset"):
    if h in r.headers:
        print(f"{h}: {r.headers[h]}")

# No headers at all? The limit is undocumented - measure it:
# send a slow ramp (1 req/s, then 2, then 4) and note where 429 starts.

如果没有有用的信息返回,用斜坡测量限制:以每秒一个请求运行一分钟,然后是两个,然后是四个,并记录 429 出现的速率。十分钟的测量胜过一周的猜测,而你找到的数字成为一切其他的基础。

带状图显示安全、边缘和触发 429 的请求间隙与每分钟 100 请求速率限制的对比
整个计算:60 秒除以公布的限制,然后为网络抖动添加缓冲。

节奏算术

取文档中的限制并除以。每分钟 100 请求的上限意味着 60 / 100 = 0.6 秒为请求之间的绝对底线——而底线不是目标。网络延迟变化,你的时钟和服务器的不同步,窗口边界的突发可以使你的表观速率加倍。目标是限制的 70-80%:在该示例中大约每请求 0.8 秒,这仍然给你每分钟 75 页。

并发性遵循相同的数字。如果你想要每秒 1.25 个请求,并且每个请求需要 2 秒往返,你需要 1.25 x 2 = 2.5 个正在进行的请求——所以是 3 个信号量,而不是你的异步代码默认的 50。未节流的异步分散是 429 的最常见原因:一次启动的一百个协程作为一个瞬时突发到达,无论平均看起来多么礼貌。如果你使用 asyncio 抓取,使用 httpx 和 aiohttp 的异步 Python 抓取中的信号量模式是解决方案。

有效的退避:指数、上限、抖动

当你确实遇到 429 时,尊重 Retry-After 如果存在。否则从一秒开始并加倍——1、2、4、8、16——直到一个硬上限,以便一个损坏的目标无法永远阻塞你的队列。然后添加抖动。没有随机化,每个在同一时刻撞墙的工作者在同一时刻重试,重现导致问题的突发。

import random, time, requests

def get_with_backoff(session, url, max_tries=6, cap=120.0):
    for attempt in range(max_tries):
        r = session.get(url, timeout=20)
        if r.status_code != 429:
            return r

        ra = r.headers.get("Retry-After", "")
        wait = float(ra) if ra.isdigit() else 2.0 ** attempt   # 1, 2, 4, 8, 16, 32
        wait = min(wait, cap)
        wait += random.uniform(0, wait * 0.3)                   # jitter: break the lockstep

        time.sleep(wait)
    raise RuntimeError(f"still 429 after {max_tries} attempts: {url}")

更好的是,闭合循环。加性增加/乘性减少给你一个抓取器,它自己找到限制并保持在限制之下:在响应干净时缓慢增加速率,一旦 429 出现就减半。每个域保持一个节奏器——限制是每个主机的,一个激进的目标不应减慢其他四十个。

class Pacer:
    """One per domain. Additive increase, multiplicative decrease."""
    def __init__(self, rps=2.0, floor=0.2, ceiling=8.0):
        self.rps, self.floor, self.ceiling = rps, floor, ceiling

    def ok(self):          # clean response: creep faster
        self.rps = min(self.ceiling, self.rps + 0.05)

    def throttled(self):   # 429: halve immediately
        self.rps = max(self.floor, self.rps / 2)

    @property
    def gap(self):
        return 1.0 / self.rps

分散负载:预算是每个身份的,而不是每个项目的

一旦你正确调整了节奏并仍然需要更多的吞吐量,唯一剩下的杠杆就是身份。因为计数器与 IP 键相关,N 个出口 IP 给你 N 倍的预算——算术就是这么简单。如果一个站点每分钟每个地址允许 60 个请求,而你需要每分钟 1,200 页,那就是 20 个并发出口在限制之下舒适运行,而不是一个出口超出限制的二十倍。

这就是旋转代理的真正用途。旋转网关从 90M+ 地址池中为每个请求分配一个不同的住宅 IP,覆盖 200+ 个国家,因此每个 IP 的计数器永远不会满。两个规则决定了分散负载和燃烧池之间的区别:即使在旋转之后也保持每个 IP 的速率低于限制(旋转乘以你的预算,而不是移除它),并为任何跨越多个请求的流程使用粘性会话——登录、购物车、分页结果集——这样会话不会中途中断。当你需要相同的 IP 几分钟后再换新的,权衡在粘性与旋转会话中列出。

用住宅 IP 乘以你的速率预算

统计面板显示避免 HTTP 429 错误的关键数字:600 毫秒最小间隙,指数退避,50% 速率削减和每个 IP 的预算
四个数字运行整个系统。从目标的限制中推导出它们,而不是从乐观中。

比更多 IP 更便宜:发送更少的请求

带宽纪律带来双重收益——更少的请求意味着更少的 429 和更小的账单,这与我们在减少抓取代理带宽成本中所做的论点相同。如果你不想构建节奏基础设施,QuantumProxies 的Scraper API在一个返回 markdown、JSON 或 HTML 的端点后吸收重试、旋转和每个域的节流。

常见问题

如何在 Python 中避免 HTTP 错误 429 请求过多?

根据目标公布的限制设置请求之间的间隔,使用信号量限制并发,大小为速率乘以延迟,出现时尊重 Retry-After,并使用指数退避加抖动重试。如果在此之后需要更多的吞吐量,将请求分散到旋转代理 IP,而不是缩短间隔。

在 429 之后我应该等待多久?

如果服务器发送了 Retry-After,就等它说的时间——可能是秒数或 HTTP 日期。没有该头的情况下,从一秒开始,每次后续 429 加倍,直到一两分钟的上限,添加随机抖动以免并行工作者同时重试。

代理能修复 429 错误吗?

它们会增加你的预算,但不会移除限制。因为计数器与客户端 IP 键相关,将运行分散到多个住宅出口可以保持每个地址在阈值之下。但一个池被以每个 IP 限制的十倍速度轰击仍会收集 429——并烧毁其声誉。先调整节奏,然后再旋转。

429 和被禁止是一样的吗?

不。429 是设计上临时的,并在窗口重置时清除,这与 403 身份阻断的区别。反复忽视它是如何使其变为永久的:持续超出正是将节流提升为更长时间 IP 禁止的信号。

为什么在第一个请求上就得到 429?

因为实际上没有被计数。一些服务器对他们认为是机器人的任何客户端返回 429,而不考虑量——状态码只是他们选择的响应。检查响应体:如果提到 robots.txt、抓取器或防火墙,修复你的头、TLS 指纹和出口 IP,而不是你的节奏。

将速率限制视为你有意花费的预算。测量上限,以其 70-80% 的速率运行,当超出时带抖动退避,只有在节奏正确后才购买更多身份。按此顺序完成,429 不再是一个错误类别,而成为配置文件中的一个数字。

获取旋转代理,停止与速率限制斗争