このガイドのゴール

  • 外部 API 呼び出し時のエラーを適切に分類し、種類ごとの対処パターンを実装する
  • Exponential Backoff によるリトライ、Circuit Breaker によるカスケード障害の防止を理解する
  • タイムアウト設計・エラーログ・Graceful Degradation を組み合わせた堅牢な連携基盤を作る

外部 API はいつか必ず落ちます。問題は「落ちるかどうか」ではなく「落ちたとき自分のサービスがどう振る舞うか」です。このガイドでは Stripe・OpenAI・SendGrid を題材に、実務で必要なエラーハンドリングパターンを整理します。

🧩 このガイドで題材にする API
異なるエラー特性を持つ 3 種の API で実装パターンを学ぶ
💳 決済系
Stripe API
冪等性キーによる安全なリトライが可能。決済失敗時は二重課金を絶対に防ぐ必要がある。
🤖 AI 推論系
OpenAI API
レスポンスが数秒〜数十秒と長く、レート制限に頻繁に到達する。タイムアウト設計が重要。
📧 メール送信系
SendGrid
送信結果が非同期で確定する。即座に成功/失敗を判定できないケースの扱い方を学ぶ。

エラーの分類と対処方針

エラーを正しく分類することが、適切な対処の第一歩です。すべてのエラーを同じようにリトライするのは、無駄であり場合によっては危険です。

分類 HTTP ステータス例 リトライ 対処
クライアントエラー 400, 401, 403, 422 しない リクエストを修正して再送。バリデーション不備やAPIキー失効
レート制限 429 する(待機あり) Retry-After ヘッダに従う。Exponential Backoff
サーバエラー 500, 502, 503 する 一時的障害の可能性。Backoff + 上限回数
タイムアウト - する(条件付き) 冪等な操作のみリトライ。非冪等は二重実行のリスク
ネットワークエラー - する DNS 障害・接続拒否など。Circuit Breaker を検討
🚨
400 番台のエラーをリトライしても解決しない

認証エラー(401)やバリデーションエラー(422)を何度リトライしても同じ結果になります。リトライ対象は 429・5xx・ネットワークエラーに限定してください。400 番台は即座にエラーログに記録し、開発者に通知すべきです。

アーキテクチャ: エラーハンドリングの全体像

🔧 外部 API 呼び出しのエラーハンドリング構成
リトライ → Circuit Breaker → フォールバックの 3 層で障害に対応
🖥️ アプリ API 呼び出し元 Circuit Breaker CLOSED: 通過 OPEN: 即座に拒否 HALF-OPEN: 試行 リトライ Exponential Backoff 最大 3〜5 回 Jitter 付き 外部 API Stripe 等 フォールバック キャッシュ / 代替処理 エラーログ・通知 再試行

Exponential Backoff の実装

リトライ間隔を指数関数的に増やすことで、障害中の API に過剰な負荷をかけることを防ぎます。

import time
import random
import httpx

def call_with_backoff(url, payload, max_retries=5):
    for attempt in range(max_retries):
        try:
            response = httpx.post(url, json=payload, timeout=30)
            if response.status_code == 429:
                retry_after = int(response.headers.get("Retry-After", 0))
                wait = max(retry_after, (2 ** attempt) + random.uniform(0, 1))
                time.sleep(wait)
                continue
            if response.status_code >= 500:
                wait = (2 ** attempt) + random.uniform(0, 1)
                time.sleep(wait)
                continue
            return response  # 成功 or クライアントエラー
        except (httpx.TimeoutException, httpx.ConnectError):
            if attempt == max_retries - 1:
                raise
            wait = (2 ** attempt) + random.uniform(0, 1)
            time.sleep(wait)
    raise Exception("Max retries exceeded")

Jitter(ランダム化)が必要な理由

複数のクライアントが同時にリトライすると、同じタイミングで API に殺到します(Thundering Herd 問題)。random.uniform(0, 1) を加えることで、リトライタイミングを分散させます。

💡
Stripe SDK にはリトライが内蔵されている

Stripe の公式 SDK は 429・5xx に対する Exponential Backoff を自動で行います。SDK を使っている場合は自前のリトライロジックを外側に重ねると二重リトライになるため注意してください。

Circuit Breaker パターン

連続して失敗している API への呼び出しを一時的に遮断し、自分のサービスのリソース(スレッド・コネクション)を保護します。

3 つの状態

状態 動作 遷移条件
CLOSED 通常通りリクエストを通す 失敗率が閾値(例: 50%)を超えたら OPEN へ
OPEN リクエストを即座に拒否(API を呼ばない) 一定時間(例: 30 秒)経過したら HALF-OPEN へ
HALF-OPEN 少数のリクエストだけ通す(テスト) 成功すれば CLOSED へ、失敗すれば OPEN に戻る
// Laravel での簡易 Circuit Breaker(Redis ベース)
class CircuitBreaker
{
    public function call(string $service, Closure $action, Closure $fallback)
    {
        $failures = Cache::get("cb:{$service}:failures", 0);
        $openUntil = Cache::get("cb:{$service}:open_until");

        if ($openUntil && now()->lt($openUntil)) {
            return $fallback(); // OPEN 状態: API を呼ばない
        }

        try {
            $result = $action();
            Cache::put("cb:{$service}:failures", 0, 300);
            return $result;
        } catch (\Exception $e) {
            $failures++;
            Cache::put("cb:{$service}:failures", $failures, 300);
            if ($failures >= 5) {
                Cache::put("cb:{$service}:open_until", now()->addSeconds(30), 60);
            }
            return $fallback();
        }
    }
}
⚠️
Circuit Breaker なしだとカスケード障害が起きる

外部 API のタイムアウトが 30 秒の場合、100 件のリクエストが同時に滞留すると 100 スレッドが 30 秒ずつ占有されます。自分のサーバのスレッドプールが枯渇し、外部 API と無関係な機能まで応答不能になります。

タイムアウトの設計

タイムアウトは「短すぎると正常なレスポンスを取りこぼし、長すぎるとスレッドが滞留する」トレードオフです。

API の種類 接続タイムアウト 読み取りタイムアウト 根拠
決済(Stripe) 5 秒 15 秒 通常 1〜3 秒で応答。15 秒あれば 3D セキュア含む処理も完了
AI 推論(OpenAI) 5 秒 120 秒 GPT-4 の長文生成は 30〜60 秒かかることがある
メール送信(SendGrid) 5 秒 10 秒 送信リクエスト自体は軽量。実際の配送は非同期
一般的な REST API 5 秒 10 秒 大半の API は 5 秒以内に応答する
# httpx でのタイムアウト設定
client = httpx.Client(
    timeout=httpx.Timeout(
        connect=5.0,
        read=30.0,
        write=5.0,
        pool=10.0,  # コネクションプール取得の待ち時間
    )
)

エラーログの設計

障害発生時に原因を特定するために、ログには十分な情報を含めます。

記録すべき項目

  • タイムスタンプ: 発生時刻(UTC)
  • API エンドポイント: どの API のどのパスへのリクエストか
  • HTTP ステータスコード: レスポンスのステータス(タイムアウト時は記録なし)
  • リクエスト ID: 外部 API が返す Request-Id ヘッダ(Stripe 等)。サポート問い合わせ時に必要
  • リトライ回数: 何回目のリトライで最終的に成功/失敗したか
  • レスポンスボディ: エラーメッセージ(ただし個人情報や認証トークンはマスク)
📌
Stripe の Request-Id は必ず記録すること

Stripe のサポートに問い合わせる際、Request-Id があると調査が大幅に早くなります。レスポンスヘッダの Request-Id をログに含めてください。OpenAI も x-request-id をヘッダで返します。

Graceful Degradation

外部 API が完全にダウンしたとき、自分のサービス全体を止めるのではなく、該当機能だけを縮退させます。

API 縮退時の動作例
OpenAI AI 回答が使えない旨を表示し、FAQ ページへ誘導
Stripe 「現在決済処理が混み合っています。しばらくしてから再度お試しください」を表示
SendGrid メール送信をキューに積んでおき、復旧後に一括送信
// Graceful Degradation の実装例
public function getAiResponse(string $prompt): string
{
    try {
        return $this->circuitBreaker->call(
            'openai',
            fn () => $this->openai->chat($prompt),
            fn () => 'ただいま AI 回答機能をご利用いただけません。FAQ ページをご確認ください。'
        );
    } catch (\Exception $e) {
        Log::error('OpenAI fallback triggered', ['error' => $e->getMessage()]);
        return 'ただいま AI 回答機能をご利用いただけません。FAQ ページをご確認ください。';
    }
}

決済 API 特有の注意点

🚨
決済リクエストのリトライは冪等性キーが必須

Stripe の課金リクエストをリトライする際は、必ず Idempotency-Key ヘッダを付けてください。同じキーで複数回リクエストしても課金は 1 回しか実行されません。キーなしでリトライすると二重課金が発生します。

import stripe
stripe.Charge.create(
    amount=1000,
    currency="jpy",
    source=token,
    idempotency_key=f"charge_{order_id}",  # 注文 ID をキーにする
)

本番チェックリスト

  • エラーを クライアントエラー / レート制限 / サーバエラー / タイムアウト に分類して処理している
  • リトライは 429 と 5xx のみ を対象にしている(400 番台はリトライしない)
  • Exponential Backoff + Jitter を実装している
  • リトライ回数に 上限(3〜5 回)を設けている
  • Circuit Breaker で連続障害時の呼び出しを遮断している
  • タイムアウト を API の特性に合わせて設定している
  • エラーログに タイムスタンプ・エンドポイント・ステータス・リクエスト ID を含めている
  • 外部 API ダウン時の Graceful Degradation を実装している
  • 決済リクエストには 冪等性キー を付与している

まとめ

  • エラーは 分類してから対処する(400 番台はリトライ不要、429・5xx はリトライ対象)
  • Exponential Backoff + Jitter でリトライ間隔を制御し、Thundering Herd を防ぐ
  • Circuit Breaker でカスケード障害を防止し、自分のサーバのリソースを保護する
  • タイムアウトは API の応答特性に合わせて 個別に設定する(AI 推論系は長めに)
  • 決済は 冪等性キー で二重課金を防ぎ、Graceful Degradation でユーザー体験を維持する

この記事の情報は 2026 年 4 月時点のものです。各 API のエラー仕様やリトライポリシーは変更される可能性があります。公式ドキュメントのエラーハンドリングセクションを併せて確認してください。