レスポンスをキャッシュして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. データの種類ごとに有効期限を決める 祝日カレンダーは数日から数週間、経済指標の発表は次の予定発表までキャッシュし、利回り曲線は1日キャッシュします。日次の為替レートは次のUTCの日付まで保持できます。この区切りを使って、有効期間、つまりTTLを決めてください。
  3. キャッシュと再試行のヘルパーを使う キャッシュキーはパスとソートしたパラメーターを組み合わせたものなので、パラメーターが違うリクエストは別のエントリになります。スクリプトは金価格を同じパスと同じTTLで2回読み取り、2回目はそのエントリが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、1分あたりの X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset に加え、クレジットに関する X-Narwhal-Cost と X-Narwhal-Balance が含まれます。

無料クレジットのアカウントは1分あたり50リクエスト、有料クレジットのアカウントは500リクエストまで送信できます。

ヘルパーはリクエストが成功した場合にのみレスポンスを保存します。期限内のエントリはHTTP呼び出しの前に返され、期限切れのエントリは再びリクエストのループに入ります。キャッシュはメモリ上にあるため、Webサービスで共有キャッシュが必要な場合はその方式を使ってください。

デフォルトのTTLは 300 秒で、金価格の呼び出しでは ttl=60 で上書きしています。各リクエストのタイムアウトは 10 秒、ループは最大 4 回まで試行します。短い時間枠の制限では、ヘルパーは Retry-After を秒数の待ち時間として読み取り、次の試行の前に待機します。このヘッダーがない場合は 2 秒待ちます。試行回数を使い切ると、ヘルパーはリクエストしたパスを付けたエラーを送出します。それ以外の失敗ステータスは、キャッシュにレスポンスを追加せず raise_for_status() に渡されます。

コードなしで再試行を処理する

Zapier、Make、n8nでは、短い待機ステップを追加するか、プラットフォームのエラー時の再試行を有効にします。これらのプラットフォームはすでにHTTPステータスを表示します。429 の場合は、待って再試行するか中止するかを決める前に、問題ボディの code を読んでください。

使うデータに合った間隔でリクエストをスケジュールします。自動化にも同じキャッシュのタイミングが当てはまります。祝日カレンダーは読み取りの間隔を数日から数週間空けられ、経済指標の発表は次の予定発表までキャッシュしておけます。リクエストが集中するときは、1分あたりの上限内に収まるよう間隔を空けてください。

状況に応じた方法の選び方

状況適した方法理由
スクリプトを1日1回実行するキャッシュは不要スクリプトがすでに1日おきに読み取っています
Webサービスが同じリクエストを繰り返すメモリ上のキャッシュまたは共有キャッシュデータの種類ごとにTTLを設定すれば、繰り返しの読み取りが料金にかかりません
多数のリクエストが一度に集中するリクエストの間隔を空ける1分あたりの上限は引き続き適用されます

TTLは、レスポンスをどれだけ再利用できるかを決めます。リクエストの間隔は、呼び出しがAPIに届くタイミングを制御します。Webサービスには両方が必要になることがあります。キャッシュは同じパスとパラメーターの繰り返しの読み取りを吸収し、間隔の調整はそれでもAPIに届く呼び出しを扱います。1時間のキャッシュを使っても、パスやパラメーターが違うリクエストは別々の呼び出しのままです。他の読み取りがキャッシュされたレスポンスを使っていても、集中したリクエストに間隔調整が必要かどうかを判断するときは、それらの呼び出しも数えてください。

429や402は課金されますか?

いいえ、エラーに課金されることはありません。429 のレート制限でも、残高不足の 402 でも同じです。エラーのボディは application/problem+json で、status、code、detail、request_id を含みます。応答の理由は code で判別できます。

残高を使い切ったときは、ダッシュボードでクレジットを追加します。短い待機ではクレジットが増えないため、ヘルパーは 402 ではリトライのループを続けずにエラーを送出します。

同じアドレスを共有するクライアントはまとめて制限されますか?

はい、同じアドレスを共有するクライアントはまとめて制限されることがあります。別の不正利用対策のバケットが、1つのネットワーク識別子に対して1分あたり120リクエスト、バーストは20まで許可します。リクエストが集中するときは、アカウントの上限とこの共有バケットの両方を考慮してください。

ほかにどのエラーを再試行すべきですか?

冪等なリクエストは、500 や 503 の後に、上限つきの指数バックオフとジッターを使って再試行します。ヘルパーは試行ごとに待ち時間を増やしてランダムな要素を加え、上限つきのリクエストループのあいだに成功するレスポンスがなければ、エラーを送出します。

400 は再試行せず、同じキーで 401 を再試行することもしません。404 series_not_found は発表後に取得できるようになる場合があるため、バックオフのループが扱うサーバーエラーとは別のケースとして扱ってください。

計算例:ダッシュボードのコスト

1,000回の呼び出しあたり$2なら、$10の無料クレジットで月5,000回、1日あたり約166回まで呼び出せます。まず、15分ごとに6つの値を読み取るダッシュボードを考えます。この更新間隔では、各値が1日96回読み取られるので、6に96を掛けて1日576リクエストになります。

次に、各値を1時間キャッシュします。その1時間のあいだのダッシュボード更新では保存済みのレスポンスを再利用するため、APIリクエストは1日144回に減ります。ダッシュボードのコストが想定を超えるときは、更新頻度を下げるという選択肢もあります。

計算1日のリクエスト数
$10の無料クレジットを、1,000回あたり$2で1日の平均に換算約166回
6つの値 × 15分更新で96回の読み取り576
同じ6つの値を、それぞれ1時間キャッシュした場合144

公開。

次のステップ