Pythonのrequests.exceptions.ReadTimeoutを修正:スクリプトのフリーズを防ぐ方法

intermediate🌐 Networking2026-07-08| Python 3.x, requestsライブラリ, すべてのOS (Linux, Windows, macOS)

Error Message

requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='api.example.com', port=443): Read timed out. (read timeout=30)
#python#requests#タイムアウト#API連携#エラーハンドリング

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)

Related Error Notes