缓存响应并应对 API 429 错误

429 rate_limit_exceeded 表示要等待 Retry-After 响应头指定的时间后再重试;402 insufficient_credits 表示余额已用完,需要充值。一个小缓存可以让重复请求不产生费用。缓存的过期时间应与数据的更新频率相匹配。

使用 API 完成操作

  1. 准备密钥 获取免费 API 密钥,并保存在 NARWHAL_API_KEY 环境变量中。Python 脚本会读取该变量,并把密钥以 Authorization: Bearer YOUR_KEY 的形式发送到 https://api.narwhalapi.com。
  2. 为每种数据类型选择过期时间 节假日日历可以缓存几天到几周,经济数据发布缓存到下一次计划发布,收益率曲线缓存一天。每日外汇汇率可以缓存到下一个 UTC 日;选择存活时间(TTL)时请使用这些边界。
  3. 使用缓存与重试辅助函数 缓存键由路径和排序后的参数组合而成,所以参数不同的请求会各有独立的缓存条目。脚本用相同的路径和 TTL 两次读取黄金价格;只要该条目仍在 TTL 内,第二次读取就使用缓存的响应。
    Python:缓存加 Retry-After 处理
    import os, random, time
    import requests
    
    API = "https://api.narwhalapi.com"
    session = requests.Session()
    session.headers["Authorization"] = f"Bearer {os.environ['NARWHAL_API_KEY']}"
    _cache: dict[str, tuple[float, dict]] = {}
    
    
    def get(path: str, ttl: int = 300, **params):
        """GET with a small in-memory cache, Retry-After on 429s and
        backoff on 5xx."""
        key = path + repr(sorted(params.items()))
        hit = _cache.get(key)
        if hit and time.time() - hit[0] < ttl:
            return hit[1]
        for attempt in range(4):
            r = session.get(API + path, params=params, timeout=10)
            if r.status_code == 402:
                raise RuntimeError("Out of credit; add credit in the dashboard.")
            if r.status_code == 429:
                time.sleep(
                    float(r.headers.get("Retry-After", 2))
                )  # short-window limit: wait, then retry
                continue
            if r.status_code in (500, 503):
                time.sleep(
                    2**attempt + random.random()
                )  # bounded exponential backoff with jitter
                continue
            r.raise_for_status()
            _cache[key] = (time.time(), r.json())
            return _cache[key][1]
        raise RuntimeError(f"Gave up on {path}")
    
    
    print(get("/v1/commodities/spot/XAU/USD", ttl=60)["price"])
    print(get("/v1/commodities/spot/XAU/USD", ttl=60)["price"], "(from cache)")
  4. 读取响应头 成功的数据响应包含 X-Request-ID、每分钟的 X-RateLimit-Limit、X-RateLimit-Remaining 和 X-RateLimit-Reset,以及与额度相关的 X-Narwhal-Cost 和 X-Narwhal-Balance。

使用免费额度的账户每分钟最多可发送 50 次请求,使用付费额度的账户为 500 次。

辅助函数只在请求成功后才存储响应。未过期的条目会在发起 HTTP 调用之前直接返回,已过期的条目则会再次进入请求循环。它的缓存位于内存中,所以当你的 Web 服务需要共享缓存时,请使用共享缓存方案。

默认 TTL 为 300 秒;黄金价格的调用用 ttl=60 覆盖了它。每个请求的超时为 10 秒,循环最多尝试 4 次。对于短时间窗口的限制,辅助函数会把 Retry-After 读作以秒为单位的延迟,并在下一次尝试前等待。如果没有该响应头,则使用 2 秒。尝试次数用完后,辅助函数会抛出带有所请求路径的错误。其他失败状态会进入 raise_for_status(),并且不会向缓存添加响应。

不写代码处理重试

在 Zapier、Make 或 n8n 中,添加一个短暂的延迟步骤,或启用平台的出错重试功能。这些平台本来就会显示 HTTP 状态。遇到 429 时,先读取问题响应体中的 code,再决定是延迟后重试还是停止。

按所用数据的特点,以合适的间隔安排请求。同样的缓存时机也适用于自动化流程:节假日日历可以隔几天到几周再读取,经济数据发布可以缓存到下一次计划发布。如果请求集中爆发,请把它们错开,使其保持在每分钟限制之内。

不同情况应选哪种方法

情况最合适的方法原因
脚本每天运行一次无需缓存脚本本身已将读取间隔了一天
Web 服务重复发起请求内存缓存或共享缓存按数据类型设置 TTL,可使重复读取不产生费用
大量请求集中爆发把请求错开每分钟限制仍然适用

TTL 决定一个响应可以被复用多久,请求间隔则控制调用何时到达 API。Web 服务可能两者都需要:缓存应对对相同路径和参数的重复读取,间隔控制则处理仍会到达 API 的调用。即使使用一小时的缓存,路径或参数不同的请求依然是各自独立的调用。判断一阵请求是否需要错开时,即使其他读取使用了缓存的响应,也要把这些调用计算在内。

429 或 402 会计费吗?

不会,错误从不计费,无论是 429 速率限制,还是余额为空的 402。错误响应体使用 application/problem+json,并包含 status、code、detail 和 request_id。code 标明了该响应的原因。

余额用完后,请在控制台中充值。遇到 402 时,辅助函数会抛出错误而不是继续重试循环,因为短暂等待并不能增加额度。

共用同一地址的客户端会被一起限流吗?

会,共用同一地址的客户端可能被一起限流:另有一个防滥用的限流桶,对同一个网络标识每分钟允许 120 次请求,突发上限为 20。请求集中爆发时,账户限制和这个共享限流桶都需要考虑。

还有哪些错误应该重试?

对幂等请求,在 500 或 503 之后用有上限的指数退避加抖动进行重试。辅助函数会逐次增加尝试之间的延迟并加入随机成分;在有上限的请求循环结束后如果仍没有成功的响应,就会抛出错误。

不要重试 400,也不要用同一个密钥重试 401。404 series_not_found 可能在某次发布之后变得可用,所以要把这种情况与退避循环处理的服务器错误分开。

计算示例:仪表板的成本

按每 1,000 次调用 $2 计算,$10 的免费额度可覆盖每月 5,000 次调用,约合每天 166 次。先看一个每 15 分钟读取 6 个值的仪表板。这样的刷新计划使每个值每天读取 96 次,所以用 6 乘以 96,得到每天 576 次请求。

然后把每个值缓存一小时。这一小时内的仪表板刷新会复用已存储的响应,API 请求减少到每天 144 次。如果仪表板的花费超出你的预期,也可以选择放慢刷新频率。

计算每日请求数
$10 免费额度按每 1,000 次调用 $2 折算为每日平均值约 166
6 个值 × 15 分钟刷新时的 96 次读取576
同样的 6 个值,各缓存一小时144

发布于。

后续步骤