J-Quants API を連打したら 58本目で HTTP 429 になりました。ところが繰り返すと、止まるまでに通る本数は 60本・30本・55本 とばらつきます。「何本まで大丈夫か」を数えるのは当てになりません。一方で毎分60本の等間隔なら、135本投げて一度も止まりませんでした。
Retry-Afterヘッダは返ってきません- 止まるのは契約単位で、全エンドポイントが同時に止まります
- 復活までは 12〜68秒(おおむね25〜30秒)
- 429の最中に叩き続けても、解除は遅れませんでした
- bulk の署名付きURLからのダウンロードは枠を食いません
429 の実物
軽いリクエスト(日足1本)を、自制なしで連続して投げました。
HTTP 429
{"message": "Rate limit exceeded. Please try again later."}本文はこれだけです。いつ再開してよいかの手がかりはありません。ヘッダも見ましたが、Retry-After・X-RateLimit-* の類は1つも返ってきませんでした。返ってくるのは Date だけです。
つまり待ち時間は自分で決めるしかありません。後述しますが、実測ではおおむね30秒で戻ります。余裕を見て60秒にしておけば、まず外しません。
★ 止まるのは「そのエンドポイント」ではなく「契約」
/equities/bars/daily を連打して429にした直後に、まったく別の /equities/master を1本だけ投げました。
| 投げたもの | 結果 |
|---|---|
/equities/bars/daily(連打したほう) | HTTP 429 |
/equities/master(一度も叩いていない) | HTTP 429 |
契約単位で、全部まとめて止まります。「日足で枠を使い切ったから、財務情報を取りにいこう」は通りません。複数のエンドポイントを並行して叩く造りにしていると、どれか1つの暴走で全部が巻き添えになります。
★ 「何本まで」は当てにならない
75秒あけてから、同じ連打を3回くり返しました。
| 通った本数 | かかった時間 | |
|---|---|---|
| 1回目 | 60本 | 6.7秒 |
| 2回目 | 30本 | 3.4秒 |
| 3回目 | 55本 | 6.1秒 |
倍近く違います。「60本投げたら止める」という実装は、2回目のような回で早々に429を踏みます。
では、どう書けば安全なのか。本数ではなく間隔で制御します。
| やり方 | 結果 |
|---|---|
| 自制なしで連打 | 30〜60本で429 |
| 毎分60本(1.00秒おき)で150秒 | 135本すべて成功。一度も止まらない |
1秒に1本なら止まりません。
地味ですが、これがいちばん確実な結論でした。「上限まで詰める」ことに意味はありません。止まったときの待ち時間のほうが、稼いだ時間より長くつきます。
復活までどれくらいか
止まってからの復活を、条件を変えて何度も測りました。
| 測り方 | 復活まで |
|---|---|
| 15秒おきに確認 | 60秒 |
| 3秒おきに確認(3回) | 68秒 / 43秒 / 12秒 |
| 3秒おきに確認(3回・別の日時) | 28秒 / 28秒 / 25秒 |
| 10秒おきに確認(3回) | 40秒 / 20秒 / 20秒 |
12秒から68秒までばらつきます。中央あたりは25〜30秒です。60秒待てば、実測した範囲ではすべて復活していました。
429の最中に叩き続けると、解除は遅れるのか
気になったので確かめました。429になったリクエストも枠に数えられるなら、リトライするほど待たされるはずです。
待ち方を2通りにして、交互に3回ずつ測りました。
| 待ち方 | 復活まで | 平均 |
|---|---|---|
| 黙って待つ(10秒ごとに1本だけ確認) | 40秒 / 20秒 / 20秒 | 27秒 |
| 叩いて待つ(3秒おきに投げ続ける) | 28秒 / 28秒 / 25秒 | 27秒 |
差はありませんでした。429の最中に叩いても、解除が遅れることはないようです。
とはいえ、叩く意味もありません。
解除が早まるわけでもないので、間隔をあけて1本ずつ確かめるのがいちばん素直です。相手のサーバーにも優しい。
★ bulk のダウンロードは枠を食わない
これは実務でいちばん効く発見でした。J-Quants の bulk(CSVのまとめ配布)は、API から署名付きURLをもらい、そのURLから直接ファイルを落とします。URLの発行先は API ですが、ファイルの置き場所は別です。
先にURLを取っておいてから、わざと429にして試しました。
| 429の最中にやったこと | 結果 |
|---|---|
| API をもう1本投げる | HTTP 429 |
| 署名付きURLから41.5MBを落とす | ○ 成功(1.5秒) |
逆向きも確かめました。同じファイルを6回続けて落としたあとに API を1本投げたところ、HTTP 200 でした。
転送量は枠に関係ありません。数えられているのは API の呼び出し回数だけです。
2年ぶん18.9GBを落とすとき、枠を消費するのはファイル1本につき署名付きURLの発行1回だけ。月次24本+日次14本なら、合計38回で済みます。大きなファイルを落とすことを恐れる必要はありません。
実装
直近60秒の本数を数えて、上限に近づいたら待つだけです。
import time
from collections import deque
class Limiter:
"""直近60秒の本数を数えて、上限に近づいたら待つ。"""
def __init__(self, per_min: int = 50):
self.per_min = per_min
self.log = deque()
def wait(self) -> None:
now = time.time()
while self.log and now - self.log[0] > 60.0:
self.log.popleft()
if len(self.log) >= self.per_min:
time.sleep(60.0 - (now - self.log[0]) + 0.05)
self.log.append(time.time())それでも踏むことはあるので、429を拾って待ち直す側も要ります。
# 429 が来たら、待ってから1回だけやり直す
for attempt in range(3):
limiter.wait()
status, body = request(path, **params)
if status != 429:
break
time.sleep(60) # Retry-After は返ってこないので自分で決める
else:
raise RuntimeError("3回試しても429でした")上限は60本ですが、自制は50本にしています。実測で30本しか通らない回があった以上、上限ぴったりに合わせる意味がないからです。
分からなかったこと
正直に書きます。枠の数え方そのものは、最後まで決着しませんでした。
分単位の固定窓(毎分00秒でリセット)なら、復活の瞬間は毎回00秒の近くに揃うはずです。止め始める位置を分の5秒・25秒・45秒と変えて測りましたが、復活したのは19秒・15秒・3秒の位置で、揃いませんでした。
- 固定窓ではなさそう(復活の秒が揃わない)
- 単純なスライド窓でもない(通る本数が30〜60でばらつく)
- 復活までの時間も一定ではない(12〜68秒)
公式に仕様の記載を見つけられなかったので、ここは「分からない」と書いておきます。実務上は1秒に1本なら止まらないことが分かれば足ります。
まとめ
- 本数を数えるのはやめる。30本で止まる回があります
- 1秒に1本。これで135本連続、一度も止まりませんでした
- 429を踏んだら60秒待つ。
Retry-Afterは来ません - 全エンドポイントが同時に止まる。並行で叩く造りは巻き添えを食います
- bulk のダウンロードは枠を食わない。消費するのは署名付きURLの発行1回だけ
レートリミットは、速く落とすための工夫より止まらない造りにしておくことのほうが効きます。1秒に1本でも、2年ぶんのティックは38回の呼び出しで揃います。
この記事の出どころ
- 対象:J-Quants API V2(
api.jquants.com/v2)。Light プラン+分足・ティックアドオン - 測り方:軽いリクエスト(日足1本)を自制なしで連投し、429が出た時点で停止。復活は間隔を変えて確認。同じ測定を日時を変えて計5回
- ダウンロードの検証:先に署名付きURLを取得 → わざと429にする → その状態で41.5MBを取得。逆に6回連続で取得したあとAPIを1本
- 環境:Windows 11 / Python 3.12 / 光回線・有線
- 測定日:2026年9月21日
- 注意:上限の値は契約・時期で変わる可能性があります。自分の環境で確かめてください。また、わざと上限を超える試験は他の利用に影響しない時間帯に行ってください