概要(リトライ処理は「一度コケても、もう一歩だけ踏み込む仕組み」)
リトライ処理は、
「一回失敗したからといって、すぐ諦めずに“もう一度やってみる”仕組み」
です。
特に自動化では、
- ネットワークが一瞬だけ不安定になる
- API サーバーが一時的に重い
- 外部サービスが「今だけちょっと調子悪い」
といった「一時的な失敗」が本当に多いです。
ここで何の工夫もなく例外をそのまま投げると、
たった一瞬のエラーのせいでバッチ全体が落ちたり、夜中の処理が止まったりします。
逆に「何でもかんでも無限リトライ」すると、
壊れているサービスをいつまでも叩き続ける迷惑なクライアントになります。
だからこそ、
- 何を失敗と見なすか
- 何回まで再試行するか
- 間隔をどうするか
- 諦める条件は何か
をちゃんと決めて、コードに落としていくことが重要です。
リトライ処理の基本的な考え方(まず頭の中を整理する)
「一時的な失敗」と「根本的な失敗」を分けて考える
リトライすべきエラーと、すべきでないエラーがあります。
一時的な失敗の例は、
- タイムアウト(たまたま遅かっただけ)
- 一時的な接続エラー(ネットワークが瞬間的に切れた)
- サーバー側の一時エラー(HTTP 503 など)
こういうものは、「もう1回やれば成功する可能性がある」ので、リトライの対象です。
一方、根本的な失敗の例は、
- 認証エラー(APIキーやパスワードが間違っている:401/403)
- そもそも存在しないリソースへのアクセス(404)
- リクエストの形式が間違っている(400)
こういうものは、何回やり直しても同じ結果になります。
これはリトライしてはいけません。「すぐ諦めて、エラーとして報告する」が正しい動きです。
まずここを明確に頭の中で分けましょう。
「何回までやるか」と「どれくらい待つか」をセットで決める
リトライ設計のコアになる数字は、この2つです。
- 最大リトライ回数(例:3回)
- 各リトライの間隔(例:2秒、4秒、8秒…)
これを適当に決めると、すぐに破綻します。
例えば、
- 1回目の失敗からすぐ 2回目 → まだサーバーが回復していない
- 3回目まで全部 1秒間隔 → サーバー側からしたら立て続けに叩かれて迷惑
など。
現実的には、
- 1〜3回程度のリトライ
- 各回の間隔は少しずつ長くしていく(指数バックオフ)
という形がよく使われます。
例1:requests での HTTP リトライ処理(基本形)
一番シンプルな「固定回数+固定間隔」パターン
まずは、HTTP GET に対して「最大3回まで、2秒間隔でリトライ」という例から。
import time
import requests
def fetch_with_retry(url: str, timeout_sec: float = 5.0, retries: int = 3, delay_sec: float = 2.0):
last_error = None
for attempt in range(1, retries + 1):
try:
resp = requests.get(url, timeout=timeout_sec)
resp.raise_for_status()
return resp
except (requests.Timeout, requests.ConnectionError) as e:
print(f"[{attempt}/{retries}] 一時的なエラー: {e}")
last_error = e
if attempt == retries:
break
time.sleep(delay_sec)
except requests.HTTPError as e:
status = e.response.status_code
print(f"HTTPエラー: {status}")
if 500 <= status < 600:
# サーバー側の一時エラーはリトライ対象にする場合が多い
last_error = e
if attempt == retries:
break
time.sleep(delay_sec)
else:
# 4xx は多くの場合リトライ不要(設計にもよる)
raise
raise last_error
Pythonここで重要なポイントを丁寧に見ていきます。
一つ目は、「例外の種類でリトライ対象を分けている」ことです。
Timeout と ConnectionError は、たいてい一時的な問題なのでリトライ。
HTTPError のうち、500番台はサーバー側都合の一時エラーとしてリトライ対象にすることが多いです。
二つ目は、「最後までダメだったら、最後のエラーを上に投げる」構造にしていることです。
リトライしてダメだったことを、呼び出し側がきちんと知れるようにするのが大事です。
三つ目は、「リトライの間で sleep している」ことです。
失敗直後に連打すると、相手にも自分にも負荷がかかります。
少し間を空けるのがマナーでもあり、成功率を上げるコツでもあります。
指数バックオフで「だんだん間隔を伸ばす」パターン
もう少し実戦的な形として、「リトライごとに待ち時間を倍にしていく」書き方を見てみましょう。
def fetch_with_backoff(url: str, timeout_sec: float = 5.0, retries: int = 3, base_delay: float = 1.0):
last_error = None
delay = base_delay
for attempt in range(1, retries + 1):
try:
resp = requests.get(url, timeout=timeout_sec)
resp.raise_for_status()
return resp
except (requests.Timeout, requests.ConnectionError, requests.HTTPError) as e:
last_error = e
print(f"[{attempt}/{retries}] エラー: {e}")
if attempt == retries:
break
time.sleep(delay)
delay *= 2 # 待ち時間を倍にする
raise last_error
Pythonこれには意味があります。
- 障害が発生してすぐは、少し間を空けてすぐ再試行したほうが、早く回復に追従できる
- それでもダメなら、相手の復旧を待つために徐々に間隔を広げていく
という発想です。
特に、たくさんのクライアントが同じサーバーにリトライをかけるような状況では、
一斉に短い間隔で叩き続けると「復旧の邪魔」になってしまいます。
例2:asyncio+aiohttp での非同期リトライ処理
非同期処理では time.sleep を絶対に使わない
asyncio の世界では、time.sleep() を使うとイベントループが止まります。
代わりに await asyncio.sleep() を使わないといけません。
aiohttp と組み合わせた例を見てみます。
import asyncio
import aiohttp
async def fetch_json_with_retry(session: aiohttp.ClientSession, url: str,
timeout_sec: float = 5.0, retries: int = 3, base_delay: float = 1.0):
last_error = None
delay = base_delay
for attempt in range(1, retries + 1):
try:
async with session.get(url, timeout=timeout_sec) as resp:
resp.raise_for_status()
return await resp.json()
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
last_error = e
print(f"[{attempt}/{retries}] 一時的なエラー: {e}")
if attempt == retries:
break
await asyncio.sleep(delay)
delay *= 2
raise last_error
Pythonここでもポイントは同じですが、非同期ならではの注意点があります。
一つ目は、「例外の型が aiohttp と asyncio 用になっている」ことです。
HTTP クライアントエラーは aiohttp.ClientError、タイムアウト系は asyncio.TimeoutError で捕まえます。
二つ目は、「待ちに await asyncio.sleep() を使っている」ことです。
これによって、リトライ待機中も他のコルーチンが動き続けることができます。
三つ目は、「この関数自体も async def である」ことです。
非同期処理全体の設計として、リトライロジックも非同期に溶け込む形にしておくと、
全体の整合性が取りやすくなります。
例3:汎用リトライデコレータで「ルールを再利用する」設計
同じリトライパターンをいろんな関数で使い回す
HTTP や DB やファイル I/O など、
あちこちで似たようなリトライ処理を書くと、コピペ地獄になります。
そういうときは、「リトライルールだけを切り出してデコレータにする」という手があります。
import time
from functools import wraps
def retry(exceptions, retries=3, base_delay=1.0):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_error = None
delay = base_delay
for attempt in range(1, retries + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_error = e
print(f"[{attempt}/{retries}] エラー: {e}")
if attempt == retries:
break
time.sleep(delay)
delay *= 2
raise last_error
return wrapper
return decorator
Pythonこれを使うと、
import requests
@retry((requests.Timeout, requests.ConnectionError), retries=3, base_delay=1.0)
def fetch_data(url):
resp = requests.get(url, timeout=5)
resp.raise_for_status()
return resp.json()
Pythonのように、「この関数はこういうエラーならリトライする」というポリシーを、
関数宣言のところにくっつけて書けます。
大事なのは次の2点です。
- 「どの例外をリトライ対象にするか」を呼び出し側で決められること
- 「リトライ回数」「待ち時間」を関数ごとに変えられること
これで、コードのあちこちに細かく散らばってしまうリトライロジックを、一つの場所に集約できます。
設計で本当に大事なところを深掘りする
ログと監視の観点:「リトライがどれくらい発生しているか」を見える化する
リトライ処理は、「動いているときほど存在を忘れられる」機能です。
でも、本番運用では「リトライがどれくらい発生しているか」がとても重要な指標になります。
例えば、
- 1日あたりのリトライ回数が急に増えた
- 特定の API だけリトライ率が異常に高い
- 特定時間帯だけタイムアウト+リトライが多い
といった情報は、「どこに問題があるか」のヒントになります。
だから、print だけでなく logging などで、
- 時刻
- 対象(URLやコマンド)
- 例外の種類
- 何回目のリトライか
を残しておくと、あとから非常に効いてきます。
「無限リトライ」を絶対にやらない
たまに「とにかく成功するまでリトライすればいいじゃん」という発想がありますが、
自動化の世界ではほぼ確実に NG です。
理由はシンプルで、
- 外部サービス側が完全に死んでいる場合、永遠に回り続けてリソースを食い潰す
- 人間が介入するチャンス(アラート)を奪ってしまう
- 「どこまでが想定内の失敗か」という境界が曖昧になる
からです。
最大回数(あるいは最大時間)を決めておき、
それを超えたら「失敗として認識させる」ことが、システムの健全さを守ります。
リトライすべきでない処理にリトライをかけない
もう一度強調しておきます。
すべての失敗にリトライをかけてはいけません。
具体例を少し具体的にします。
ユーザー入力が間違っている(バリデーションエラー)
→ 何度送っても同じなので、リトライは無意味
APIキーが無効(HTTP 401 / 403)
→ 設定ミスやセキュリティ問題なので、すぐに人間が気づくべき
存在しない ID にアクセス(HTTP 404)
→ 入力データかコードのバグ。リトライでは治らない
ファイルが存在しないエラー
→ パス間違い・事前処理の抜け漏れ。やはり人間や設計で直すべき
こういうものまでリトライの対象にしてしまうと、
「本当は設計や運用で直すべき問題」が、リトライによって隠されてしまいます。
リトライは「一時的な環境要因の揺らぎ」を吸収するためのものであって、
設計やデータの間違いを隠すためのものではありません。
まとめ(リトライ処理は「優しさ」ではなく「戦略」)
リトライ処理を、自動化の現場目線でまとめると、こうなります。
リトライすべきは「一時的な失敗」(タイムアウト・接続エラー・一部5xx)であり、
「根本的な失敗」(認証エラー・バリデーションエラーなど)はすぐに失敗として扱う。
最大回数と待ち時間(できれば指数バックオフ)を設計して、無限リトライは絶対にしない。
requests / aiohttp / subprocess / DB など、それぞれで「どの例外をリトライ対象にするか」を明確に決める。
リトライの発生状況をログに残し、「どこでどれくらい揺らいでいるか」を後から見えるようにする。
うまく設計されたリトライ処理は、「たまに転ぶけど、ちゃんと立ち上がって前に進み続ける」自動化を支える骨格になります。
