TL;DR: クイックフィックス
ほとんどの ReadTimeout エラーは、requests ライブラリがデフォルトで無制限に待機するために発生します。これを修正するには、常に秒単位で明示的な timeout 値を渡すようにしてください。もしスクリプトがすでに特定の値でタイムアウトしている場合は、サーバーに余裕を持たせるために 60 秒や 90 秒に増やしてみてください。
import requests
try:
# レスポンスの遅いAPIエンドポイントに対して60秒のタイムアウトを使用
response = requests.get('https://api.example.com/data', timeout=60)
response.raise_for_status()
except requests.exceptions.ReadTimeout:
print("The server is taking too long to respond. Try again later.")
なぜサーバーから応答がなくなるのか?
ReadTimeout は、レストランで席に着き、料理を注文したのに、厨房から前菜すら運ばれてこないまま1時間待たされるようなものだと考えてください。サーバーへの接続には成功しましたが、制限時間内にデータが送られてこなかったということです。
これは、玄関のドアを通り抜けることさえできない ConnectTimeout とは異なります。リードタイムアウトが発生している場合、サーバーはおそらくリクエストを受信したものの、処理が滞っています。これには通常、いくつかの具体的な理由があります:
- **データベースのボトルネック:** サーバーが実行に45秒以上かかる巨大なSQLクエリを実行している。
- **リソースの枯渇:** APIに過度な負荷がかかっており(フラッシュセール中など)、リクエストが長いキュー(待ち行列)に並んでいる。
- **巨大なペイロード:** 一度に50,000件のレコードを要求したため、サーバーがそれほど大量のJSONのシリアライズに苦労している。
- **ネットワーク品質の低下:** 高いパケット損失により、通信の途中で接続が停滞している。
ステップバイステップの解決策
1. きめ細かなタイムアウト設定を使用する
単一の数値ではなく、timeout にタプルを渡します。これにより、初期接続のための短い時間枠と、実際のデータ転送のための長い時間枠を個別に設定できます。プロのヒント:接続タイムアウトには 3.05 秒を使用してください。これは TCP パケットの再送サイクルと一致するためです。
# (接続タイムアウト, リードタイムアウト)
# 接続に3.05秒、実際のデータの到着に60秒待機
response = requests.get('https://api.example.com', timeout=(3.05, 60))
2. 堅牢なリトライ戦略を構築する
ネットワークの一時的な不具合はよくあることです。スクリプトをクラッシュさせるのではなく、HTTPAdapter を使用して、エクスポネンシャルバックオフ(指数関数的後退)を伴うリクエストの自動リトライを実行しましょう。これは、負荷の高いサーバーを追い詰めないように、試行の間隔を徐々に長くする(例:0.6秒、1.2秒、2.4秒)ことを意味します。
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def get_resilient_session(retries=3, backoff=0.3):
session = requests.Session()
retry_strategy = Retry(
total=retries,
read=retries,
connect=retries,
backoff_factor=backoff,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
# 諦める前に最大3回リトライします
client = get_resilient_session()
response = client.get('https://api.example.com', timeout=30)
3. ペイロードを削減する
APIを制御できる場合、またはAPIがフィルタリングをサポートしている場合は、一度にすべてを要求するのをやめましょう。1,000項目を一気に要求するのはタイムアウトの原因になります。レスポンス時間を10秒以内に抑えるために、リクエストを小さなチャンクに分割してください。
- **ページネーション**を実装する(例:`?limit=50&offset=0`)。
- **フィールドフィルタリング**を使用して、実際に必要なキーのみをダウンロードする。
- データ転送を圧縮するために、サーバーが `Accept-Encoding: gzip` をサポートしているか確認する。
4. 大容量のレスポンスをストリーミングする
100MB以上のファイルをダウンロードする場合、すべてがメモリにロードされるのを待たないでください。stream=True を使用して、データが到着するたびに処理します。これにより、RAMがいっぱいになる間に接続がタイムアウトするのを防げます。
with requests.get('https://api.example.com/big-export.csv', stream=True, timeout=60) as r:
r.raise_for_status()
for chunk in r.iter_content(chunk_size=1024 * 8):
# 一度に8KBずつ処理
save_to_disk(chunk)
タイムアウトロジックのテスト方法
コードをテストするために、実際のサーバー障害を待つ必要はありません。httpbin.org を使用して、オンデマンドで遅いレスポンスをシミュレートできます。
- **失敗をトリガーする:** サーバーに10秒待機するように要求しますが、タイムアウトを5秒に設定します。
```
import requests try: requests.get('https://httpbin.org/delay/10', timeout=5) except requests.exceptions.ReadTimeout: print("Success: The timeout was caught!")
- **成功を確認する:** 同じ10秒の遅延に対して、タイムアウトを15秒に設定します。これでリクエストはステータスコード 200 で正常に完了するはずです。
## 参考文献
- [Requests公式ガイド: タイムアウト](https://requests.readthedocs.io/en/latest/user/advanced/#timeouts)
- [urllib3 Retry ドキュメント](https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html#urllib3.util.Retry)

