API を使う以上、レート制限と一時的なエラーは避けられません。本記事では、J-Quants API でのリトライ設計の基本を、指数バックオフの実装と生成AIに任せがちな落とし穴とあわせて整理します。

目次

  1. エラーの種類と対処方針
  2. 指数バックオフ
  3. Python での実装
  4. 401 はリトライしない
  5. 連続呼び出しのペース配分
  6. ログとモニタリング
  7. 生成AIに任せがちな落とし穴

エラーの種類と対処方針

API でよく出るエラーと、対処の方向性をまとめます。

ステータス意味リトライすべきか
200成功
400リクエスト形式不正しない(コード修正)
401API キーが無効・未設定しない(キーの値とヘッダ名を確認)
403権限なし(プラン外)しない(プラン確認)
404リソースなししない(パス・パラメータ確認)
429レート制限する(時間を置く)
500 / 502 / 503 / 504サーバ側エラーする(短期的な不具合の可能性)

「とりあえずリトライ」は危険です。400 系の多くは原因が消えないため、リトライしてもエラーが続きます

指数バックオフ

レート制限やサーバ側エラーで再試行するときは、指数バックオフ(exponential backoff)が定番です。

待機時間を回数ごとに 2 倍ずつ伸ばし、ランダム性(jitter)を加えます。

waitn=min(base×2n+U(0,j),max)\text{wait}_n = \min\left(\text{base} \times 2^n + U(0, j), \text{max}\right)
  • nn : 試行回数(0 始まり)
  • base\text{base} : 初回待機時間
  • U(0,j)U(0, j) : 0 〜 jj の一様乱数
  • max\text{max} : 上限(待ちすぎ防止)

ジッタを加えるのは、複数クライアントが同時にリトライして再衝突するのを避けるためです。

Python での実装

requeststime.sleep を使う最小実装です。

import os
import random
import time
import requests
BASE_URL = "https://api.jquants.com/v2"
def request_with_retry(
method: str,
url: str,
*,
headers: dict | None = None,
params: dict | None = None,
json_body: dict | None = None,
max_retries: int = 5,
base_wait: float = 1.0,
max_wait: float = 60.0,
timeout: float = 30.0,
) -> requests.Response:
"""指数バックオフ付きで HTTP リクエストを行う。"""
retryable_status = {429, 500, 502, 503, 504}
for attempt in range(max_retries + 1):
try:
response = requests.request(
method,
url,
headers=headers,
params=params,
json=json_body,
timeout=timeout,
)
except (requests.ConnectionError, requests.Timeout):
# ネットワーク系の例外は一時的なことが多いのでリトライ対象
if attempt == max_retries:
raise
else:
if response.status_code < 400:
return response
if response.status_code not in retryable_status:
# 400 系の多くは再試行しても直らないので即座に上げる
response.raise_for_status()
if attempt == max_retries:
response.raise_for_status()
# ここに来るのはリトライするケース
wait = min(base_wait * (2 ** attempt), max_wait) + random.uniform(0.0, 1.0)
time.sleep(wait)
# 到達しないが念のため
raise RuntimeError("リトライ上限に達しました")

要点は次の 3 つです。

  • リトライ対象のステータスを明示(429 と 5xx)
  • 400 系は即座に例外(リトライしても無駄)
  • ネットワーク例外も一時的とみなしてリトライ

401 はリトライしない

認証は x-api-key ヘッダに API キーを乗せるだけで、キーは失効しません。そのため 401 は「キーが無効・未設定」を意味し、待っても再試行しても直りません。401 は即座に例外として上げて設定を見直す のが正しい流れです。

class JQuantsClient:
BASE_URL = "https://api.jquants.com/v2"
def __init__(self, api_key: str):
self._headers = {"x-api-key": api_key}
def get(self, path: str, params: dict | None = None) -> dict:
response = request_with_retry(
"GET",
f"{self.BASE_URL}{path}",
headers=self._headers,
params=params,
)
# 401(キー無効・未設定)は request_with_retry の対象外なので
# ここまで来た時点で raise_for_status により例外化される
response.raise_for_status()
return response.json()
# API キーは環境変数から(ハードコードしない)
client = JQuantsClient(api_key=os.environ["JQUANTS_API_KEY"])
data = client.get("/equities/master")
print(f"件数: {len(data.get('data', []))}")

連続呼び出しのペース配分

レート制限に当たる前に、呼び出し側でペース配分 するとさらに安定します。

import time
class RateLimiter:
"""直近 1 秒あたりの呼び出し数を上限に抑えるシンプルな実装。"""
def __init__(self, calls_per_second: float):
self._min_interval = 1.0 / calls_per_second
self._last_call = 0.0
def wait(self) -> None:
elapsed = time.time() - self._last_call
if elapsed < self._min_interval:
time.sleep(self._min_interval - elapsed)
self._last_call = time.time()
limiter = RateLimiter(calls_per_second=2.0)
codes = ["7203", "9984", "6758"]
for code in codes:
limiter.wait()
# client.get("/equities/bars/daily", params={"code": code, ...})

公式のレート制限値はプランによって異なるため、自分のプランで許される値より少し低めに設定 しておくと安全です。

ログとモニタリング

リトライ設計を入れたら、何が起きているかを記録します。最低限あると助かるのは次の 3 つです。

  • リクエスト URL・パラメータ(機微情報は除く)
  • レスポンスのステータスコード
  • リトライ回数・累計待機時間
import logging
logger = logging.getLogger(__name__)
def request_with_retry_logged(method, url, **kwargs):
# 認証情報やパラメータの値そのものは出さない
logger.info("API request start: %s %s", method, url)
response = request_with_retry(method, url, **kwargs)
logger.info("API request done: status=%s", response.status_code)
return response

ログレベルは INFO 以上にまとめておくと、本番運用でも騒がしくなりすぎません。

生成AIに任せがちな落とし穴

生成AI にリトライコードを書かせると、便利な一方で次のような問題を起こしがちです。

  • すべてのエラーを except Exception で一律リトライしてしまう(400 系も叩き続ける)
  • ジッタを忘れて固定間隔でリトライ(複数プロセスで再衝突しやすい)
  • 無限リトライ(while True の中で try のみ、上限なし)
  • キー無効の 401 を、ただのバックオフで処理(待っても直らない)
  • API キーの値をログに出してしまう(マスクが抜ける)

生成AI の出力を採用するときは、少なくとも次の点を確認 します。

  • リトライ対象のステータスが明示されているか
  • 上限回数(max_retries)があるか
  • ジッタが入っているか
  • 401 をリトライ対象に含めていないか(キー無効は待っても直らない)
  • API キーがログ・例外メッセージに混ざっていないか

注意点

  • 公式が示す制限値が最新の正(本記事の値はあくまで目安)
  • 大量の銘柄を一気に回すスクリプトは、走り出す前に ドライラン で件数を見積もる
  • 失敗した銘柄をログに残し、後で再試行できるようにする
  • レート制限に当たり続けるなら、ローカルキャッシュ(#6-7「取得データのローカル保存とキャッシュ戦略」)で頻度を減らす方向に倒す

生成AI へのプロンプト例

リトライ付きの J-Quants クライアントを生成する例です。

J-Quants API のクライアント JQuantsClient を Python で書いてください。
要件:
- 認証は API キーを環境変数 JQUANTS_API_KEY から読み、x-api-key ヘッダに乗せる(ハードコードしない)
- ベース URL は https://api.jquants.com/v2
- HTTP リクエストは指数バックオフ + ジッタ付きでリトライ
- リトライ対象: 429, 500, 502, 503, 504 と requests.ConnectionError / Timeout
- 最大 5 回、初期待機 1 秒、最大待機 60 秒
- 401(キー無効・未設定)/ 400 / 403 / 404 は即座に例外を上げる
- 1 秒あたりの呼び出し回数を抑えるレートリミッタを内蔵
- ログには URL とステータスコードのみ出力、認証情報は記録しない
- pandas 2.2 / requests 前提、docstring は日本語

まとめ

  • リトライしていいエラーは 429 と 5xx + 一時的なネットワーク例外
  • 指数バックオフ + ジッタが基本パターン
  • 401(キー無効・未設定)はリトライ対象外。設定を見直す
  • レート制限値より少し低めに自前で抑えると安定
  • 生成AI に任せた実装は「対象エラー」「上限回数」「ジッタ」「認証ログ」を必ず確認