429 rate_limit_exceeded 表示要等待 Retry-After 响应头指定的时间后再重试;402 insufficient_credits 表示余额已用完,需要充值。一个小缓存可以让重复请求不产生费用。缓存的过期时间应与数据的更新频率相匹配。
使用 API 完成操作
- 准备密钥 获取免费 API 密钥,并保存在
NARWHAL_API_KEY环境变量中。Python 脚本会读取该变量,并把密钥以Authorization: Bearer YOUR_KEY的形式发送到https://api.narwhalapi.com。 - 为每种数据类型选择过期时间 节假日日历可以缓存几天到几周,经济数据发布缓存到下一次计划发布,收益率曲线缓存一天。每日外汇汇率可以缓存到下一个 UTC 日;选择存活时间(TTL)时请使用这些边界。
- 使用缓存与重试辅助函数 缓存键由路径和排序后的参数组合而成,所以参数不同的请求会各有独立的缓存条目。脚本用相同的路径和 TTL 两次读取黄金价格;只要该条目仍在 TTL 内,第二次读取就使用缓存的响应。
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)") - 读取响应头 成功的数据响应包含
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 |
发布于。