---
title: "遇到 429 后该等多久？解析 Retry-After 并检查剩余预算"
date: 2026-11-13
slug: http-retry-after-budget
canonical: https://kernelranch.com/blog/2026/11/http-retry-after-budget
markdown_url: https://kernelranch.com/blog/2026/11/http-retry-after-budget.md
tags: ["http", "api", "resilience"]
---收到 HTTP 429 时，`Retry-After` 可能是秒数，也可能是 HTTP 日期。先把提示转换成等待时长，再与本次操作剩余预算比较。服务端要求等 60 秒而调用方只剩 2 秒时，应停止本次重试或交给有独立预算的异步任务。

## 1. 用固定时间验证等待决策

保存为 `retry_after_lab.py`，用 Python 运行。例中约定非法或缺失头部使用本地退避，并为下一次请求保留两秒。这里的日期解析使用 Python 邮件日期解析器做容错，只接受带时区的结果。

```python
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
import re


def retry_delay(header, received_at):
    if header is None:
        return None
    value = header.strip(" \t")
    if re.fullmatch(r"[0-9]+", value):
        digits = value.lstrip("0")
        return float(digits or "0") if len(digits) <= 12 else float("inf")

    try:
        target = parsedate_to_datetime(value)
    except (ValueError, OverflowError):
        return None
    if target.tzinfo is None:
        return None
    return max(0.0, (target - received_at).total_seconds())


def plan_retry(header, received_at, remaining, backoff):
    hint = retry_delay(header, received_at)
    delay = max(backoff, hint) if hint is not None else backoff
    return delay if delay + 2 <= remaining else None


now = datetime(2026, 11, 13, 12, tzinfo=timezone.utc)
cases = [
    ("seconds", "5", 5),
    ("date", "Fri, 13 Nov 2026 12:00:04 GMT", 4),
    ("past date", "Fri, 13 Nov 2026 11:59:00 GMT", 1),
    ("invalid", "-1", 1),
    ("missing", None, 1),
    ("over budget", "60", None),
]
for label, header, expected in cases:
    delay = plan_retry(header, now, remaining=8, backoff=1)
    assert delay == expected
    print(f"{label}: {'stop' if delay is None else f'wait {delay:g}s'}")
```

```text
seconds: wait 5s
date: wait 4s
past date: wait 1s
invalid: wait 1s
missing: wait 1s
over budget: stop
```

## 2. 把等待放回请求生命周期

RFC 6585 允许 429 附带这个头，RFC 9110 同时定义了 503 上的等待含义。收到日期形式时，用接收响应的墙钟换算等待时长。执行等待和衡量总预算时用单调时钟，以免系统校时改变截止时间。样本中的超大秒数转换为无穷大，在有限预算下会停止重试。

重试入口还需确认操作可重放。读请求可以按服务合同重试，写请求则需要按具体接口确认幂等保证，例如 [130](/blog/2026/11/post-idempotency-key.md) 的幂等键。多客户端同时退避时，可在服务端等待下限之外加入随机抖动，并在等待后重算剩余预算。记录状态码、解析结果和停止原因，能够分辨服务端要求延后与客户端预算耗尽。

## 小结

把 Retry-After 当作重试的等待下限。是否真的重试还取决于操作语义和剩余预算。

## 数据来源与检索时间

| 来源 | URL | 检索日期 | 对应内容 |
|---|---|---|---|
| RFC 9110 §10.2.3 | https://www.rfc-editor.org/rfc/rfc9110.html#section-10.2.3 | 2026-11-13 | Retry-After 的两种格式及 503 语义 |
| RFC 6585 §4 | https://www.rfc-editor.org/rfc/rfc6585.html#section-4 | 2026-11-13 | 429 可携带 Retry-After |
| Python email.utils | https://docs.python.org/3.11/library/email.utils.html#email.utils.parsedate_to_datetime | 2026-11-13 | 日期解析及无时区返回值 |

## 核验说明

- Windows、Python 3.11.9 固定接收时间，核对六种决策分支。示例只做计算，未执行网络请求或真实等待。
