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

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

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

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

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

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

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

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

```python
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 出现的速率。十分钟的测量胜过一周的猜测，而你找到的数字成为一切其他的基础。

![以每分钟 100 次请求为限的请求间隔对比：无间隔的突发请求会触发 429，0.6 秒的间隔是绝对下限，0.8 秒是安全目标，仍能每分钟抓取 75 页；卡片展示了计算过程：60 除以 100，再为随机抖动留出余量。](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335152/blog/generated/qp-b9-012-diag1.png)
*整个计算：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 抓取](https://quantumproxies.io/blog/python-async-scraping-httpx-aiohttp-proxies)中的信号量模式是解决方案。

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

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

```python
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 出现就减半。每个域保持一个节奏器——限制是每个主机的，一个激进的目标不应减慢其他四十个。

```python
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 个并发出口在限制之下舒适运行，而不是一个出口超出限制的二十倍。

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

[用住宅 IP 乘以你的速率预算](https://quantumproxies.io/residential-proxies)

![负载分散到多个出口 IP 上，每个 IP 保持在每分钟 60 次请求的上限以内，因此 20 个出口每分钟可抓取 1,200 页；卡片列出了驱动整个系统的四个数字：每分钟 100 次时 0.6 s 的最小间隔，带随机抖动的 1、2、4、8、16 秒退避，收到 429 时速率减半，以及每个 IP 的配额。](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335151/blog/generated/qp-b9-012-diag2.png)
*四个数字运行整个系统。从目标的限制中推导出它们，而不是从乐观中。*

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

- 在获取之前去重。大多数抓取队列包含相同的 URL，带有跟踪参数和尾部斜杠——首先规范化。
- 使用条件 GET。发送 If-Modified-Since 或 If-None-Match，304 让你只花费一小部分带宽，但仍然算作一个请求。
- 优先使用 JSON 端点而不是渲染的 HTML。一个 API 调用通常可以替代十个页面抓取，并且 API 限制通常有文档说明。
- 在开发期间积极缓存。对保存的响应重新运行解析器不花费请求和阻断。
- 在目标的本地时区的非高峰时段抓取。相同的速率在凌晨 3 点对服务器的影响较小，且被节流的频率更低。
- 只抓取变化的内容。一个每周更新一次的目录的每日完整抓取是六次浪费的抓取。

带宽纪律带来双重收益——更少的请求意味着更少的 429 和更小的账单，这与我们在[减少抓取代理带宽成本](https://quantumproxies.io/blog/reduce-proxy-bandwidth-costs-scraping)中所做的论点相同。如果你不想构建节奏基础设施，QuantumProxies 的[Scraper API](https://quantumproxies.io/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 不再是一个错误类别，而成为配置文件中的一个数字。

[获取旋转代理，停止与速率限制斗争](https://quantumproxies.io/rotating-proxies)
