WebやAPIにアクセスする際、レスポンスが返らず処理が止まってしまうと、画面は固まり、バッチは遅延し、障害の切り分けも難しくなります。
Pythonのrequests
ではタイムアウトを明示的に指定しない限り無期限に待機します。
この記事では、タイムアウト設定の重要性と正しい使い方、よくある落とし穴、実践的なベストプラクティスを、初心者の方にも分かりやすく丁寧に解説します。
requestsのタイムアウトはなぜ必須か
デフォルトは無期限(待ち続けて処理が止まる)
requestsのデフォルトはタイムアウト無し(None)です。
つまり、サーバーが応答しない場合でも永遠に待ち続ける可能性があります。
これは一時的なネットワーク障害や相手サーバーの不調で簡単に発生し、UIではフリーズのように見え、バッチ処理ではキュー詰まりやリソース枯渇につながります。
ユーザー体験とサーバー負荷を守る
ユーザーに「遅い」体験を与えないためには、一定時間で諦めてエラーメッセージや再試行の案内を返すことが大切です。
タイムアウトを適切に設定すれば、フロントエンドは即座に代替表示ができ、バックエンドはスレッドや接続の無駄な占有を減らせます。
結果として、システム全体の安定性が向上します。
ネットワーク障害や遅延に強くする
インターネット経由のAPIは、DNS解決、TCP接続、TLSハンドシェイク、データ転送など多段の工程があります。
各工程で遅延や停止が起こり得るため、適切な接続タイムアウトと読み取りタイムアウトを分けて設定し、異常時には速やかに復旧処理(リトライやフェイルオーバー)に移れるようにします。
基本の使い方と例
timeout引数の基本(timeout=5の例)
もっとも基本的な指定はtimeout=秒数
です。
この値は「接続と読み取りの両方に同じ秒数」を適用します。
# 基本: 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)
のタプルで指定すると、接続確立までとレスポンス受信(読み取り)のタイムアウトを分けられます。
たとえば接続は素早く諦めたいが、大きなレスポンスの読み取りは長めに待ちたいケースに有効です。
# 接続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を捕まえる(例外処理の書き方)
タイムアウトは例外で通知されます。
例外を捕捉し、メッセージや再試行に繋げるのが定石です。
# タイムアウトを明確に捕捉してハンドリングする例
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秒」を意味します。
短い時間を指定したい場合は小数(浮動小数)を使います。
# 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秒/バッチは長め)
用途により妥当な値は異なります。
最初の仮置きとして、以下を参考にしてください。
用途 | 接続タイムアウト | 読み取りタイムアウト | コメント |
---|---|---|---|
ユーザー向けUI | 1〜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
は、ホストが存在しない、接続が拒否された、途中で接続が切れたなど、時間切れ以外の接続問題を指します。
事象/症状 | 主な例外 | 典型的な原因 | 対策の方向性 |
---|---|---|---|
接続が確立できない | ConnectTimeout | DNS遅延、ファイアウォール、ネットワーク断 | 接続TO短縮、DNS/ルーティング確認 |
接続後にデータが来ない | ReadTimeout | サーバー処理遅延、帯域輻輳、スロットリング | 読み取りTO調整、リトライ、バックオフ |
すぐに失敗 | ConnectionError | 接続拒否、SSL失敗、名前解決失敗 | 設定/証明書/URLを見直し |
DNSや接続で詰まる場合はconnect timeout
DNS解決やTCPハンドシェイクで時間がかかるケースではconnect timeout
を短めに設定します。
たとえば到達不能なIPに対して次のようにすると、素早く諦められます。
# 到達不能アドレスへの接続で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
に引っかかりやすくなります。
読み取りタイムアウトは「無通信の空白時間」に対する制限であり、全体の処理時間の上限ではありません。
大容量転送では、チャンクごとにデータが届いている限りタイムアウトせず、しばらくデータが来ないときにのみ発火します。
必要に応じて読み取り側を長めに設定しましょう。
# 大きなレスポンスをストリーミングで読む例(読み取り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)
downloaded bytes: 104857600
タイムアウトとリトライはセットで設計する
タイムアウトは「諦める基準」であり、リトライ戦略と組み合わせて全体の成功率を高めるのが実務では一般的です。
指数バックオフや上限回数を設定し、相手サーバーの負荷を悪化させないよう配慮します。
# 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ユーティリティを用意して付け忘れを防止します。
# 付け忘れ防止の薄いラッパー例
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/delete
にtimeout
を付ける方針にします。
コードレビューや静的解析のルール化も効果的です。
秒数は根拠を持って決めてログに残す
単に「5秒」にするのではなく、RTT(往復遅延)、SLA、過去実績を根拠に設定し、実測ログを取りながら調整します。
計測はtime.monotonic()
で簡単に実装できます。
# 所要時間をログに残して根拠ある調整を可能にする例
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を踏まえて少し長めに設定する、といった切り替えを環境変数で行うと便利です。
# 環境に応じてタイムアウトを切り替える例
# (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-except
でConnectTimeout
/ReadTimeout
を正しく捕捉します。
値はUIなら数秒、バッチなら長めから始め、実測ログをもとに調整します。
さらに、タイムアウトとリトライをセットで設計し、環境変数による切り替えやラッパー関数で付け忘れを防止すると、運用面の安心感が大きく高まります。
今日から全てのrequests
呼び出しにタイムアウトを明示し、安定したAPI連携を実現しましょう。