閉じる

【Python】requestsのタイムアウト設定は必須!例と注意点

WebやAPIにアクセスする際、レスポンスが返らず処理が止まってしまうと、画面は固まり、バッチは遅延し、障害の切り分けも難しくなります。

Pythonのrequestsではタイムアウトを明示的に指定しない限り無期限に待機します。

この記事では、タイムアウト設定の重要性と正しい使い方、よくある落とし穴、実践的なベストプラクティスを、初心者の方にも分かりやすく丁寧に解説します。

requestsのタイムアウトはなぜ必須か

デフォルトは無期限(待ち続けて処理が止まる)

requestsのデフォルトはタイムアウト無し(None)です。

つまり、サーバーが応答しない場合でも永遠に待ち続ける可能性があります。

これは一時的なネットワーク障害や相手サーバーの不調で簡単に発生し、UIではフリーズのように見え、バッチ処理ではキュー詰まりやリソース枯渇につながります。

ユーザー体験とサーバー負荷を守る

ユーザーに「遅い」体験を与えないためには、一定時間で諦めてエラーメッセージや再試行の案内を返すことが大切です。

タイムアウトを適切に設定すれば、フロントエンドは即座に代替表示ができ、バックエンドはスレッドや接続の無駄な占有を減らせます

結果として、システム全体の安定性が向上します。

ネットワーク障害や遅延に強くする

インターネット経由のAPIは、DNS解決、TCP接続、TLSハンドシェイク、データ転送など多段の工程があります。

各工程で遅延や停止が起こり得るため、適切な接続タイムアウトと読み取りタイムアウトを分けて設定し、異常時には速やかに復旧処理(リトライやフェイルオーバー)に移れるようにします。

基本の使い方と例

timeout引数の基本(timeout=5の例)

もっとも基本的な指定はtimeout=秒数です。

この値は「接続と読み取りの両方に同じ秒数」を適用します。

Python
# 基本: 5秒でタイムアウトするGET
import requests
import time

url = "https://httpbin.org/get"  # 動作検証用のパブリックAPI

start = time.monotonic()
resp = requests.get(url, timeout=5)  # 5秒で接続/読み取りの両方がタイムアウト
elapsed = time.monotonic() - start

print("status:", resp.status_code)
print(f"elapsed: {elapsed:.2f}s")
print("ok:", resp.ok)
実行結果
status: 200
elapsed: 0.23s
ok: True

接続と読み取りを分ける(connect, readのタプル)

timeout=(connect_timeout, read_timeout)のタプルで指定すると、接続確立までレスポンス受信(読み取り)のタイムアウトを分けられます。

たとえば接続は素早く諦めたいが、大きなレスポンスの読み取りは長めに待ちたいケースに有効です。

Python
# 接続3.05秒、読み取り10秒に分けて設定する例
import requests

url = "https://httpbin.org/delay/5"  # 5秒遅延してからレスポンスを返す

# 接続には通常1秒台〜数秒で十分、読み取りは用途により長めに
resp = requests.get(url, timeout=(3.05, 10))
print("status:", resp.status_code)
実行結果
status: 200

どのように効くのか

  • 接続タイムアウトはDNS解決やTCP接続の確立までの待ち時間に影響します。
  • 読み取りタイムアウトは、接続後にサーバーからデータが届くまでの待ち時間(無通信の空白時間)に影響します。

try-exceptでTimeoutを捕まえる(例外処理の書き方)

タイムアウトは例外で通知されます。

例外を捕捉し、メッセージや再試行に繋げるのが定石です。

Python
# タイムアウトを明確に捕捉してハンドリングする例
import requests

url = "https://httpbin.org/delay/5"

try:
    # 接続は短め、読み取りはそこそこ長め
    resp = requests.get(url, timeout=(2, 3))
    resp.raise_for_status()  # HTTPエラー(4xx/5xx)なら例外
    print("success:", resp.json())
except requests.exceptions.ConnectTimeout:
    print("接続がタイムアウトしました(connect timeout)")
except requests.exceptions.ReadTimeout:
    print("読み取りがタイムアウトしました(read timeout)")
except requests.exceptions.Timeout:
    # 上記のサブクラスをまとめて扱いたい場合の保険
    print("タイムアウトが発生しました")
except requests.exceptions.HTTPError as e:
    print("HTTPエラー:", e)
except requests.exceptions.RequestException as e:
    # すべてのrequests例外の最上位。ネットワーク関連のその他の失敗
    print("その他の通信エラー:", e)
実行結果
読み取りがタイムアウトしました(read timeout)

単位は秒(ミリ秒ではない)に注意

timeoutに指定する値は秒です。

ミリ秒ではありません。

たとえばtimeout=500は「500秒」を意味します。

短い時間を指定したい場合は小数(浮動小数)を使います。

Python
# 200ミリ秒待ちたい → 0.2秒を指定する
import requests
from requests.exceptions import ReadTimeout

try:
    requests.get("https://httpbin.org/delay/1", timeout=0.2)
except ReadTimeout:
    print("0.2秒では待ちきれずにタイムアウトしました")
実行結果
0.2秒では待ちきれずにタイムアウトしました

値の目安(UIは3〜10秒/バッチは長め)

用途により妥当な値は異なります。

最初の仮置きとして、以下を参考にしてください。

用途接続タイムアウト読み取りタイムアウトコメント
ユーザー向けUI1〜3秒3〜10秒UX優先。遅いときはフィードバックやリトライ導線を出す。
バックグラウンド(バッチ)3〜10秒15〜60秒大きなレスポンスやスロットリングを考慮して長めに。
同一VPC/社内ネットワーク0.5〜2秒2〜5秒近距離かつ安定網なら短めに設定できる。
大容量ダウンロード2〜5秒30〜300秒転送の無通信間隔が長くなり得るため読み取りを十分長く。

あくまで目安なので、実測(ログ)に基づいて調整することが重要です。

よくあるエラーと注意点

TimeoutとConnectionErrorの違い

requests.exceptions.Timeoutは時間切れによる失敗です。

一方でrequests.exceptions.ConnectionErrorは、ホストが存在しない、接続が拒否された、途中で接続が切れたなど、時間切れ以外の接続問題を指します。

事象/症状主な例外典型的な原因対策の方向性
接続が確立できないConnectTimeoutDNS遅延、ファイアウォール、ネットワーク断接続TO短縮、DNS/ルーティング確認
接続後にデータが来ないReadTimeoutサーバー処理遅延、帯域輻輳、スロットリング読み取りTO調整、リトライ、バックオフ
すぐに失敗ConnectionError接続拒否、SSL失敗、名前解決失敗設定/証明書/URLを見直し

DNSや接続で詰まる場合はconnect timeout

DNS解決やTCPハンドシェイクで時間がかかるケースではconnect timeoutを短めに設定します。

たとえば到達不能なIPに対して次のようにすると、素早く諦められます。

Python
# 到達不能アドレスへの接続でconnect timeoutを短めにする
import requests
from requests.exceptions import ConnectTimeout

try:
    # 10.255.255.1 は多くの環境で到達不能なダミー
    requests.get("http://10.255.255.1", timeout=(1, 5))
except ConnectTimeout:
    print("接続確立までに1秒でタイムアウト(ConnectTimeout)")
実行結果
接続確立までに1秒でタイムアウト(ConnectTimeout)

大きいレスポンスはread timeoutに影響

巨大なJSONやファイルをダウンロードすると、ネットワーク状況によってはread timeoutに引っかかりやすくなります。

読み取りタイムアウトは「無通信の空白時間」に対する制限であり、全体の処理時間の上限ではありません

大容量転送では、チャンクごとにデータが届いている限りタイムアウトせず、しばらくデータが来ないときにのみ発火します。

必要に応じて読み取り側を長めに設定しましょう。

Python
# 大きなレスポンスをストリーミングで読む例(読み取りTOを長めに)
import requests

url = "https://nbg1-speed.hetzner.com/100MB.bin"  # 大容量テストファイル(例)
with requests.get(url, stream=True, timeout=(3, 60)) as r:
    r.raise_for_status()
    total = 0
    for chunk in r.iter_content(chunk_size=1024 * 128):  # 128KBごとに読む
        if not chunk:
            continue
        total += len(chunk)
    print("downloaded bytes:", total)

配信元サーバーの転送速度が最大でも70Mbps前後しかないので、完了まで12~18秒程度かかります。

実行結果
downloaded bytes: 104857600

タイムアウトとリトライはセットで設計する

タイムアウトは「諦める基準」であり、リトライ戦略と組み合わせて全体の成功率を高めるのが実務では一般的です。

指数バックオフや上限回数を設定し、相手サーバーの負荷を悪化させないよう配慮します。

Python
# requests + urllib3 Retry でリトライを組み合わせる例
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()

retry = Retry(
    total=3,                # 最大3回リトライ
    backoff_factor=0.5,     # 0.5, 1.0, 2.0秒...と指数的に待つ
    status_forcelist=[429, 500, 502, 503, 504],  # 一時的エラー対象
    allowed_methods=["HEAD", "GET", "OPTIONS", "POST"]  # リトライ許可メソッド
)

adapter = HTTPAdapter(max_retries=retry)
session.mount("http://", adapter)
session.mount("https://", adapter)

try:
    # タイムアウトは毎回明示。Totalではなく(connect, read)で指定
    r = session.get("https://httpbin.org/status/500", timeout=(2, 5))
    r.raise_for_status()
    print("success")
except requests.exceptions.RequestException as e:
    print("失敗:", e)
実行結果
失敗: 500 Server Error: INTERNAL SERVER ERROR for url: https://httpbin.org/status/500

毎回timeoutを明示する(グローバル設定はない)

requestsにはグローバルなデフォルトタイムアウトの設定がありません

毎回の呼び出しでtimeout=...を指定するか、自前のラッパー関数/Sessionユーティリティを用意して付け忘れを防止します。

Python
# 付け忘れ防止の薄いラッパー例
import os
import requests
from typing import Tuple

def default_timeout() -> Tuple[float, float]:
    # 環境変数で上書き可能にする
    connect = float(os.getenv("REQUEST_TIMEOUT_CONNECT", "2.0"))
    read = float(os.getenv("REQUEST_TIMEOUT_READ", "5.0"))
    return (connect, read)

def safe_get(url: str, **kwargs) -> requests.Response:
    # timeout未指定なら自動付与
    if "timeout" not in kwargs:
        kwargs["timeout"] = default_timeout()
    return requests.get(url, **kwargs)

resp = safe_get("https://httpbin.org/get")  # 常にtimeoutが付く
print(resp.status_code)
実行結果
200

シンプルなベストプラクティス

すべてのrequestsにtimeoutを付与する

タイムアウト無しの呼び出しは禁止と捉えて、すべてのget/post/put/deletetimeoutを付ける方針にします。

コードレビューや静的解析のルール化も効果的です。

秒数は根拠を持って決めてログに残す

単に「5秒」にするのではなく、RTT(往復遅延)、SLA、過去実績を根拠に設定し、実測ログを取りながら調整します。

計測はtime.monotonic()で簡単に実装できます。

Python
# 所要時間をログに残して根拠ある調整を可能にする例
import time
import logging
import requests

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("http")

def timed_get(url: str, timeout=(2, 5)):
    t0 = time.monotonic()
    try:
        r = requests.get(url, timeout=timeout)
        r.raise_for_status()
        return r
    finally:
        elapsed = time.monotonic() - t0
        logger.info("GET %s timeout=%s elapsed=%.2fs", url, timeout, elapsed)

r = timed_get("https://httpbin.org/delay/1", timeout=(1, 3))
print("status:", r.status_code)
実行結果
INFO:http:GET https://httpbin.org/delay/1 timeout=(1, 3) elapsed=1.08s
status: 200

環境変数で値を切り替える(dev/prod)

開発環境では短め、本番では実運用の遅延とSLAを踏まえて少し長めに設定する、といった切り替えを環境変数で行うと便利です。

Python
# 環境に応じてタイムアウトを切り替える例
# (dev: 短め / prod: 長め)
import os
import requests

ENV = os.getenv("APP_ENV", "dev")

DEFAULT_TIMEOUTS = {
    "dev": (1.0, 3.0),
    "stg": (2.0, 5.0),
    "prod": (3.0, 10.0),
}

def get_timeout():
    base = DEFAULT_TIMEOUTS.get(ENV, DEFAULT_TIMEOUTS["dev"])
    # さらに個別上書きも可能
    c = float(os.getenv("REQUEST_TIMEOUT_CONNECT", base[0]))
    r = float(os.getenv("REQUEST_TIMEOUT_READ", base[1]))
    return (c, r)

resp = requests.get("https://httpbin.org/get", timeout=get_timeout())
print("env:", ENV, "timeout:", get_timeout(), "status:", resp.status_code)
実行結果
env: dev timeout: (1.0, 3.0) status: 200

外部APIのSLAや制限に合わせて調整する

外部APIのSLA、レート制限、応答時間のベースラインに合わせてタイムアウトとリトライを設計します。

レートリミット超過(429)時はバックオフを長めにし、業務の締め切り(ジョブの全体時間)も考慮して「何回までリトライできるか」を具体的に決めます。

最後に、実装時に覚えておきたい補足です。

requestsのtimeoutは「合計時間の上限」ではありません

接続と読み取りに適用されること、ストリーミング読み取りではチャンク間の無通信時間に対して発火することを意識してください。

全体の制限が必要な場合は、外部でトータルタイムアウトを管理する(呼び出し側でタイマーを持つ)などの工夫も有効です。

まとめ

requestsのタイムアウト設定は「必須」です

理由は明快で、デフォルトが無期限のため、異常時に処理が止まり、UXとシステムの安定性を損なうからです。

基本はtimeout=(connect, read)で分けて指定し、try-exceptConnectTimeout/ReadTimeoutを正しく捕捉します。

値はUIなら数秒、バッチなら長めから始め、実測ログをもとに調整します。

さらに、タイムアウトとリトライをセットで設計し、環境変数による切り替えやラッパー関数で付け忘れを防止すると、運用面の安心感が大きく高まります。

今日から全てのrequests呼び出しにタイムアウトを明示し、安定したAPI連携を実現しましょう。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!