额度与限流

有两层限制,请分清:

1. 客户端限流(你的 key)

每个 key 默认 60 次/分钟。超出返回 429 rate_limited,并带 Retry-After 头(秒)。

2. 上游 CourtListener 额度(整个服务共享)

本服务使用 CourtListener 免费账户,上游额度很稀缺:

窗口 上游限额 本服务实际使用(留 10% 余量)
每分钟 5 4
每小时 50 45
每日 125 112

这些额度由所有客户端共享,另有一部分后台日预算(默认 80 次/24 小时)专供自动同步和补全文。因此:

接口 消耗
GET /search?source=courtlistener 1 次(结果缓存 30 分钟)
POST /citations/verify 1 次(缓存 24 小时)
GET /dockets/{id} 首次 1 次,之后永久缓存
GET /opinions/cl:{id}(本地没有时) 最多 3 次(opinion、cluster、docket),之后入库;正文优先取法院 PDF,不耗额度
GET /govinfo/uscourts/recent?live=true 0(走 GovInfo,不占 CourtListener)

每个响应都带头 X-Upstream-Quota-Remaining:当前还能安全使用的上游调用数(取各窗口最小值)。GET /v1/quota 返回详细的窗口用量。

遇到 429 怎么办

{"error": {"code": "rate_limited", "message": "..."}}
  1. 读取 Retry-After 头,等待相应秒数后重试(指数退避,加抖动)。
  2. 本服务对按需上游请求最多排队约 8 秒,仍无额度才返回 429。
  3. 优先改用本地接口(source=local 搜索、/opinions/recent);这些永不触发上游限额。
  4. 对同一篇判例避免重复请求,客户端侧缓存结果。
import time, httpx
def get(url, **kw):
    for attempt in range(5):
        r = httpx.get(url, **kw)
        if r.status_code != 429:
            return r
        time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
    return r
async function get(url, opts, tries = 5) {
  for (let i = 0; i < tries; i++) {
    const r = await fetch(url, opts);
    if (r.status !== 429) return r;
    const wait = Number(r.headers.get("Retry-After") || 2 ** i);
    await new Promise(res => setTimeout(res, wait * 1000));
  }
  throw new Error("still rate limited");
}