T-CREATOR

Dify のコール失敗を解決:429/5xx/タイムアウト時の再試行とバックオフ戦略

Dify のコール失敗を解決:429/5xx/タイムアウト時の再試行とバックオフ戦略

Dify を使って AI アプリケーションを本番運用する際、避けて通れないのが API コール失敗への対応です。特に OpenAI や Anthropic などの LLM プロバイダーへのリクエストは、レート制限エラー(429)、サーバーエラー(5xx)、タイムアウトといった一時的な障害が発生しやすく、適切な再試行戦略を実装しないとユーザー体験が著しく損なわれてしまいます。

本記事では、Dify における API コール失敗の原因と解決策を徹底解説します。エラーごとの特性を理解し、効果的なバックオフアルゴリズムとリトライロジックを実装することで、安定した本番運用を実現しましょう。

背景

Dify における API コールの仕組み

Dify は LLM アプリケーション開発プラットフォームとして、複数のサービスと連携してワークフローを実行します。

以下の図は、Dify における典型的な API コールフローを示しています。

mermaidflowchart LR
  user["ユーザー"] -->|リクエスト| dify["Dify<br/>ワークフロー"]
  dify -->|API コール| llm["LLM Provider<br/>(OpenAI/Anthropic)"]
  dify -->|API コール| vectordb["Vector DB<br/>(Pinecone/Weaviate)"]
  dify -->|API コール| external["外部 API<br/>(カスタム)"]

  llm -->|レスポンス| dify
  vectordb -->|レスポンス| dify
  external -->|レスポンス| dify
  dify -->|結果| user

図で理解できる要点

  • Dify は複数の外部サービスに依存したアーキテクチャ
  • 各 API コールは独立した障害点となりえる
  • 1 つのワークフローで複数のコール失敗リスクが存在

API コール失敗が発生する主な理由

Dify のワークフロー実行中に API コールが失敗する理由は多岐にわたります。

#エラー種別HTTP ステータス発生原因一時的/恒久的
1レート制限エラー429 Too Many RequestsAPI 呼び出し頻度の上限超過一時的
2サーバーエラー500 Internal Server Errorプロバイダー側の内部エラー一時的
3サービス利用不可503 Service Unavailableメンテナンスや高負荷状態一時的
4ゲートウェイタイムアウト504 Gateway Timeoutプロキシやロードバランサーでのタイムアウト一時的
5接続タイムアウト-ネットワーク遅延や接続失敗一時的

これらのエラーのほとんどは一時的な障害であり、適切な再試行戦略を実装することで解決できます。

再試行戦略が重要な理由

再試行なしでコール失敗をそのまま返してしまうと、以下の問題が発生します。

  • ユーザー体験の低下: 一時的なエラーでもワークフローが中断
  • データ損失: 実行中の処理状態が失われる
  • 運用コスト増加: 手動での再実行が必要になる

一方で、無計画な再試行は逆効果です。

  • サーバー負荷の悪化: 短時間での大量リトライがプロバイダーに負担
  • カスケード障害: 再試行の連鎖が障害を拡大
  • コスト増大: 無駄な API コールによる課金増加

適切なバックオフ戦略を実装することで、これらのトレードオフをバランスよく解決できます。

課題

Dify のデフォルト動作の限界

Dify のデフォルト設定では、API コール失敗時の再試行が十分でない場合があります。

typescript// Dify のデフォルト的な挙動(簡略化)
async function callLLMAPI(request) {
  try {
    const response = await fetch(apiEndpoint, {
      method: 'POST',
      body: JSON.stringify(request),
      timeout: 30000, // 30秒
    });

    return response;
  } catch (error) {
    // エラーをそのまま上位に投げる
    throw error;
  }
}

このコードの問題点は以下の通りです。

  • 再試行ロジックがない: 1 回のコール失敗でワークフロー全体が停止
  • エラー種別の判断なし: 429 と 500 を区別せず同じ扱い
  • 待機時間の考慮なし: すぐにエラーを返すため回復の機会がない

エラーコード 429: Rate Limit Exceeded の特性

レート制限エラーは、API プロバイダーが設定している呼び出し頻度の上限を超えた際に発生します。

javascriptError 429: Rate limit exceeded for requests
You are sending requests too quickly. Please pace your requests.

{
  "error": {
    "message": "Rate limit reached for default-gpt-4 in organization org-xxx",
    "type": "rate_limit_error",
    "param": null,
    "code": "rate_limit_exceeded"
  }
}

発生条件

  • リクエスト数/分 (RPM: Requests Per Minute) の超過
  • トークン数/分 (TPM: Tokens Per Minute) の超過
  • 同時実行数 (Concurrent Requests) の超過

エラーメッセージ内の重要情報

多くの API プロバイダーは、レスポンスヘッダーに再試行までの待機時間を含めています。

yamlHTTP/1.1 429 Too Many Requests
Retry-After: 20
X-RateLimit-Limit-Requests: 500
X-RateLimit-Remaining-Requests: 0
X-RateLimit-Reset-Requests: 2024-10-31T12:30:45Z

エラーコード 5xx: Server Error の特性

5xx 系エラーは、API プロバイダー側のサーバーで問題が発生していることを示します。

vbnetError 500: Internal Server Error
The server encountered an unexpected condition.

Error 503: Service Unavailable
The service is temporarily unavailable. Please try again later.

500 エラーの特徴

  • プロバイダーの内部エラー(バグやリソース不足)
  • 再試行で成功する可能性がある
  • Retry-After ヘッダーがない場合が多い

503 エラーの特徴

  • メンテナンスや高負荷による一時的な停止
  • Retry-After ヘッダーが含まれる場合が多い
  • 長時間継続する可能性がある

タイムアウトエラーの特性

タイムアウトは、指定時間内にレスポンスが返ってこない場合に発生します。

javascriptError: Request timeout
The request to https://api.openai.com/v1/chat/completions timed out after 30000ms

Error 504: Gateway Timeout
The upstream server failed to respond in time.

タイムアウトの種類

#タイムアウト種別説明典型的な値
1接続タイムアウトTCP 接続確立までの制限時間10 秒
2読み取りタイムアウトレスポンス受信開始までの制限時間30 秒
3全体タイムアウトリクエスト全体の制限時間60 秒

LLM API は処理時間が長いため、タイムアウト設定が短すぎると正常なリクエストでも失敗します。

解決策

再試行戦略の基本設計

効果的な再試行戦略は、以下の 3 つの要素で構成されます。

mermaidflowchart TD
  start["API コール"] -->|失敗| check["エラー種別<br/>判定"]
  check -->|429 エラー| wait429["Retry-After<br/>待機"]
  check -->|5xx エラー| wait5xx["Exponential<br/>Backoff"]
  check -->|タイムアウト| waitTimeout["固定待機"]
  check -->|その他| fail["失敗通知"]

  wait429 --> retry{"再試行回数<br/>上限チェック"}
  wait5xx --> retry
  waitTimeout --> retry

  retry -->|上限以内| start
  retry -->|上限超過| fail

再試行戦略の 3 要素

  1. エラー分類: どのエラーを再試行対象とするか
  2. 待機戦略: どのくらい待ってから再試行するか
  3. 上限設定: 何回まで再試行を許可するか

Exponential Backoff(指数バックオフ)の実装

最も一般的で効果的なバックオフアルゴリズムは、Exponential Backoff with Jitter です。

基本的な指数バックオフ

待機時間を指数関数的に増加させる手法です。

typescript/**
 * 基本的な指数バックオフの計算
 * @param retryCount - 現在の再試行回数(0から開始)
 * @param baseDelay - 基本待機時間(ミリ秒)
 * @returns 待機時間(ミリ秒)
 */
function calculateExponentialBackoff(
  retryCount: number,
  baseDelay: number = 1000
): number {
  // 2^retryCount * baseDelay
  return Math.pow(2, retryCount) * baseDelay;
}

このアルゴリズムの待機時間は以下のように増加します。

#再試行回数計算式待機時間
10 回目2^0 × 1000ms1 秒
21 回目2^1 × 1000ms2 秒
32 回目2^2 × 1000ms4 秒
43 回目2^3 × 1000ms8 秒
54 回目2^4 × 1000ms16 秒

Jitter(ジッター)の追加

複数のクライアントが同時に再試行すると、サーバーに負荷が集中します。これを防ぐため、待機時間にランダムな揺らぎ(Jitter)を加えます。

typescript/**
 * Jitter付き指数バックオフの計算
 * @param retryCount - 現在の再試行回数
 * @param baseDelay - 基本待機時間(ミリ秒)
 * @param maxDelay - 最大待機時間(ミリ秒)
 * @returns 待機時間(ミリ秒)
 */
function calculateExponentialBackoffWithJitter(
  retryCount: number,
  baseDelay: number = 1000,
  maxDelay: number = 60000
): number {
  // 指数バックオフの基本計算
  const exponentialDelay =
    Math.pow(2, retryCount) * baseDelay;

  // 最大値でキャップ
  const cappedDelay = Math.min(exponentialDelay, maxDelay);

  // Full Jitter: 0 から cappedDelay の間のランダム値
  const jitteredDelay = Math.random() * cappedDelay;

  return Math.floor(jitteredDelay);
}

Jitter のメリット

  • サンダリングハード現象の防止: 複数クライアントの再試行が分散
  • サーバー負荷の軽減: リクエストが時間的に広がる
  • 成功率の向上: 競合が減ることで個々のリクエストが成功しやすくなる

上限値の設定

無限に待機時間が増えるのを防ぐため、最大待機時間を設定します。

typescript/**
 * キャップ付き指数バックオフ
 */
const config = {
  baseDelay: 1000, // 1秒
  maxDelay: 60000, // 60秒
  maxRetries: 5, // 最大5回再試行
};

function getBackoffDelay(retryCount: number): number {
  if (retryCount >= config.maxRetries) {
    throw new Error('Maximum retry count exceeded');
  }

  return calculateExponentialBackoffWithJitter(
    retryCount,
    config.baseDelay,
    config.maxDelay
  );
}

エラー種別ごとの再試行戦略

エラーの種類によって、最適な再試行戦略は異なります。

429 エラーの処理

レート制限エラーは、Retry-After ヘッダーを優先的に使用します。

typescript/**
 * 429 エラー専用の待機時間計算
 * @param headers - レスポンスヘッダー
 * @param retryCount - 再試行回数
 * @returns 待機時間(ミリ秒)
 */
function calculate429BackoffDelay(
  headers: Headers,
  retryCount: number
): number {
  // Retry-After ヘッダーを確認
  const retryAfter = headers.get('Retry-After');

  if (retryAfter) {
    // 秒数で指定されている場合
    const seconds = parseInt(retryAfter, 10);
    if (!isNaN(seconds)) {
      return seconds * 1000; // ミリ秒に変換
    }

    // 日時で指定されている場合
    const retryDate = new Date(retryAfter);
    if (!isNaN(retryDate.getTime())) {
      const delay = retryDate.getTime() - Date.now();
      return Math.max(delay, 0);
    }
  }

  // Retry-After がない場合は指数バックオフ
  return calculateExponentialBackoffWithJitter(
    retryCount,
    2000,
    120000
  );
}

処理フロー

  1. Retry-After ヘッダーの存在確認
  2. ヘッダーがあればその値を使用
  3. ヘッダーがなければ指数バックオフ(より長めの待機時間)

5xx エラーの処理

サーバーエラーは、指数バックオフを使用しつつ、早めに諦める戦略が有効です。

typescript/**
 * 5xx エラー専用の再試行設定
 */
const serverErrorConfig = {
  baseDelay: 500, // 0.5秒(429より短め)
  maxDelay: 30000, // 30秒
  maxRetries: 3, // 最大3回(429より少なめ)
};

/**
 * 5xx エラーの待機時間計算
 */
function calculate5xxBackoffDelay(
  statusCode: number,
  retryCount: number
): number {
  // 503 の場合は Retry-After を考慮
  if (statusCode === 503) {
    // 実装は 429 と同様
  }

  return calculateExponentialBackoffWithJitter(
    retryCount,
    serverErrorConfig.baseDelay,
    serverErrorConfig.maxDelay
  );
}

5xx エラーの再試行方針

  • 短めの待機時間: サーバーが迅速に回復する可能性に賭ける
  • 少ない再試行回数: 長時間障害の場合は早めに諦める
  • 503 は特別扱い: メンテナンス情報があれば活用

タイムアウトの処理

タイムアウトは、ネットワーク問題か処理時間超過かを区別します。

typescript/**
 * タイムアウトエラーの再試行設定
 */
const timeoutConfig = {
  baseDelay: 1000,
  maxDelay: 10000, // 10秒(比較的短め)
  maxRetries: 2, // 最大2回
  timeoutIncrease: 1.5, // タイムアウト時間を50%増加
};

/**
 * タイムアウト時の処理
 */
function handleTimeoutError(
  currentTimeout: number,
  retryCount: number
): { delay: number; newTimeout: number } {
  // 次回のタイムアウト時間を延長
  const newTimeout = Math.floor(
    currentTimeout * timeoutConfig.timeoutIncrease
  );

  // 待機時間は短めに
  const delay = calculateExponentialBackoffWithJitter(
    retryCount,
    timeoutConfig.baseDelay,
    timeoutConfig.maxDelay
  );

  return { delay, newTimeout };
}

タイムアウト再試行の特徴

  • タイムアウト時間の延長: 次回は処理完了を待つ
  • 短い待機時間: ネットワーク回復は早いと仮定
  • 少ない再試行: 長時間処理は諦める

再試行不可能なエラーの判定

すべてのエラーを再試行すべきではありません。

typescript/**
 * 再試行可能なエラーかどうかを判定
 * @param error - エラーオブジェクト
 * @returns 再試行可能な場合 true
 */
function isRetryableError(error: any): boolean {
  // HTTP ステータスコードがある場合
  if (error.status) {
    const status = error.status;

    // 再試行可能なステータスコード
    const retryableStatuses = [
      408, // Request Timeout
      429, // Too Many Requests
      500, // Internal Server Error
      502, // Bad Gateway
      503, // Service Unavailable
      504, // Gateway Timeout
    ];

    return retryableStatuses.includes(status);
  }

  // ネットワークエラー
  if (
    error.code === 'ECONNRESET' ||
    error.code === 'ETIMEDOUT' ||
    error.code === 'ENOTFOUND'
  ) {
    return true;
  }

  // その他は再試行不可
  return false;
}

再試行すべきでないエラー

#ステータス説明理由
1400 Bad Requestリクエスト形式が不正修正なしに成功しない
2401 Unauthorized認証情報が無効API キーの問題
3403 Forbiddenアクセス権限なし権限設定の問題
4404 Not Foundエンドポイントが存在しないURL の問題
5422 Unprocessable Entityパラメータが不正リクエスト内容の問題

具体例

TypeScript による完全な実装例

実用的な再試行ロジックを TypeScript で実装します。

リトライ可能なフェッチ関数の基本構造

typescript/**
 * リトライ設定の型定義
 */
interface RetryConfig {
  maxRetries: number;
  baseDelay: number;
  maxDelay: number;
  timeoutMs: number;
}

/**
 * リトライ状態の型定義
 */
interface RetryState {
  attempt: number;
  lastError: Error | null;
  totalDelay: number;
}

次に、設定のデフォルト値を定義します。

typescript/**
 * デフォルトのリトライ設定
 */
const DEFAULT_RETRY_CONFIG: RetryConfig = {
  maxRetries: 5,
  baseDelay: 1000,
  maxDelay: 60000,
  timeoutMs: 30000,
};

メインのリトライロジック

typescript/**
 * リトライ機能付き fetch 関数
 * @param url - リクエスト URL
 * @param options - fetch オプション
 * @param config - リトライ設定
 * @returns レスポンス
 */
async function fetchWithRetry(
  url: string,
  options: RequestInit = {},
  config: Partial<RetryConfig> = {}
): Promise<Response> {
  // 設定をマージ
  const finalConfig: RetryConfig = {
    ...DEFAULT_RETRY_CONFIG,
    ...config,
  };

  // 状態を初期化
  const state: RetryState = {
    attempt: 0,
    lastError: null,
    totalDelay: 0,
  };

  // リトライループ
  while (state.attempt <= finalConfig.maxRetries) {
    try {
      // タイムアウト設定を追加
      const controller = new AbortController();
      const timeoutId = setTimeout(
        () => controller.abort(),
        finalConfig.timeoutMs
      );

      // リクエスト実行
      const response = await fetch(url, {
        ...options,
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      // 成功レスポンスをチェック
      if (response.ok) {
        return response;
      }

      // エラーレスポンスの処理
      await handleErrorResponse(
        response,
        state,
        finalConfig
      );
    } catch (error: any) {
      // 例外の処理
      await handleException(error, state, finalConfig);
    }

    state.attempt++;
  }

  // 最大リトライ回数を超えた
  throw new Error(
    `Failed after ${finalConfig.maxRetries} retries. ` +
      `Last error: ${state.lastError?.message}`
  );
}

このコードは、リトライループの基本構造を実装しています。タイムアウト制御とエラーハンドリングを分離することで、読みやすさを保っています。

エラーレスポンスの処理

typescript/**
 * エラーレスポンスを処理し、適切な待機を行う
 */
async function handleErrorResponse(
  response: Response,
  state: RetryState,
  config: RetryConfig
): Promise<void> {
  const status = response.status;

  // 再試行不可能なエラー
  if (!isRetryableStatus(status)) {
    const errorBody = await response.text();
    throw new Error(
      `Non-retryable error ${status}: ${errorBody}`
    );
  }

  // 429 エラーの特別処理
  if (status === 429) {
    const delay = calculate429BackoffDelay(
      response.headers,
      state.attempt
    );

    console.warn(
      `Rate limit hit (429). Waiting ${delay}ms before retry ${
        state.attempt + 1
      }`
    );

    await sleep(delay);
    state.totalDelay += delay;
    state.lastError = new Error(
      `Rate limit exceeded (429)`
    );
    return;
  }

  // 5xx エラーの処理
  if (status >= 500 && status < 600) {
    const delay = calculate5xxBackoffDelay(
      status,
      state.attempt
    );

    console.warn(
      `Server error (${status}). Waiting ${delay}ms before retry ${
        state.attempt + 1
      }`
    );

    await sleep(delay);
    state.totalDelay += delay;
    state.lastError = new Error(`Server error (${status})`);
    return;
  }

  // その他の再試行可能エラー
  const delay = calculateExponentialBackoffWithJitter(
    state.attempt,
    config.baseDelay,
    config.maxDelay
  );

  await sleep(delay);
  state.totalDelay += delay;
  state.lastError = new Error(`HTTP error ${status}`);
}

ステータスコードごとに異なる待機戦略を適用することで、効率的な再試行を実現しています。

例外の処理

typescript/**
 * 例外(タイムアウトやネットワークエラー)を処理
 */
async function handleException(
  error: any,
  state: RetryState,
  config: RetryConfig
): Promise<void> {
  // AbortController によるタイムアウト
  if (error.name === 'AbortError') {
    const { delay, newTimeout } = handleTimeoutError(
      config.timeoutMs,
      state.attempt
    );

    console.warn(
      `Request timeout. Waiting ${delay}ms before retry ${
        state.attempt + 1
      }. ` + `New timeout: ${newTimeout}ms`
    );

    // 次回のタイムアウトを延長
    config.timeoutMs = newTimeout;

    await sleep(delay);
    state.totalDelay += delay;
    state.lastError = error;
    return;
  }

  // ネットワークエラー
  if (isRetryableError(error)) {
    const delay = calculateExponentialBackoffWithJitter(
      state.attempt,
      config.baseDelay,
      config.maxDelay
    );

    console.warn(
      `Network error: ${error.message}. ` +
        `Waiting ${delay}ms before retry ${
          state.attempt + 1
        }`
    );

    await sleep(delay);
    state.totalDelay += delay;
    state.lastError = error;
    return;
  }

  // 再試行不可能な例外
  throw error;
}

タイムアウトの場合は次回のタイムアウト時間を延長することで、長時間処理にも対応しています。

ユーティリティ関数

typescript/**
 * 指定時間だけ待機
 */
function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * ステータスコードが再試行可能かチェック
 */
function isRetryableStatus(status: number): boolean {
  return [408, 429, 500, 502, 503, 504].includes(status);
}

Dify ワークフロー内での活用例

実際の Dify ワークフローで、カスタム API ノードとしてリトライロジックを組み込む例を示します。

カスタム API ノードの実装

typescript/**
 * Dify カスタムノード: LLM API 呼び出し(リトライ付き)
 */
interface DifyNodeInput {
  prompt: string;
  model: string;
  temperature: number;
}

interface DifyNodeOutput {
  response: string;
  tokensUsed: number;
  retryCount: number;
  totalDelay: number;
}

ノードの入出力型を定義しました。次に、メイン処理を実装します。

typescript/**
 * LLM API を呼び出すカスタムノード
 */
async function callLLMWithRetry(
  input: DifyNodeInput
): Promise<DifyNodeOutput> {
  const startTime = Date.now();
  let retryCount = 0;
  let totalDelay = 0;

  // リトライ設定(LLM向けにカスタマイズ)
  const config: RetryConfig = {
    maxRetries: 3,
    baseDelay: 2000, // 2秒
    maxDelay: 120000, // 2分
    timeoutMs: 60000, // 60秒
  };

  try {
    const response = await fetchWithRetry(
      'https://api.openai.com/v1/chat/completions',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
        },
        body: JSON.stringify({
          model: input.model,
          messages: [
            { role: 'user', content: input.prompt },
          ],
          temperature: input.temperature,
        }),
      },
      config
    );

    const data = await response.json();

    return {
      response: data.choices[0].message.content,
      tokensUsed: data.usage.total_tokens,
      retryCount,
      totalDelay,
    };
  } catch (error: any) {
    // 最終的に失敗した場合のエラーハンドリング
    console.error(
      'LLM API call failed after retries:',
      error
    );

    throw new Error(
      `Failed to call LLM API: ${error.message}. ` +
        `Retries: ${retryCount}, Total delay: ${totalDelay}ms`
    );
  }
}

並列 API コールのリトライ戦略

複数の API を並列で呼び出す場合、個別のリトライを管理します。

typescript/**
 * 複数の LLM API を並列で呼び出す
 */
async function callMultipleLLMs(
  prompts: string[]
): Promise<DifyNodeOutput[]> {
  // 各プロンプトを並列で処理
  const promises = prompts.map(async (prompt) => {
    return callLLMWithRetry({
      prompt,
      model: 'gpt-4',
      temperature: 0.7,
    });
  });

  // Promise.allSettled で部分的な失敗に対応
  const results = await Promise.allSettled(promises);

  // 成功したものだけを抽出
  const successful = results
    .filter(
      (r): r is PromiseFulfilledResult<DifyNodeOutput> =>
        r.status === 'fulfilled'
    )
    .map((r) => r.value);

  // 失敗したものをログ出力
  results
    .filter(
      (r): r is PromiseRejectedResult =>
        r.status === 'rejected'
    )
    .forEach((r, index) => {
      console.error(
        `Failed to process prompt ${index}: ${r.reason.message}`
      );
    });

  return successful;
}

Promise.allSettled を使用することで、一部の API コールが失敗しても他の処理を継続できます。

監視とログ出力の実装

本番運用では、リトライの発生状況を監視することが重要です。

typescript/**
 * リトライイベントのログ記録
 */
interface RetryLogEntry {
  timestamp: string;
  url: string;
  attempt: number;
  errorType: string;
  statusCode?: number;
  delay: number;
  totalDelay: number;
}

/**
 * リトライログを記録
 */
function logRetry(entry: RetryLogEntry): void {
  // 構造化ログとして出力
  console.log(
    JSON.stringify({
      level: 'warn',
      message: 'API retry occurred',
      ...entry,
    })
  );

  // メトリクス送信(Prometheus, DataDog など)
  // metrics.incrementCounter('api_retry_count', {
  //   url: entry.url,
  //   error_type: entry.errorType,
  // });
}

ログを構造化することで、後から分析しやすくなります。

リトライ統計の収集

typescript/**
 * リトライ統計情報
 */
class RetryStatistics {
  private stats = new Map<
    string,
    {
      totalCalls: number;
      totalRetries: number;
      totalDelay: number;
      errorCounts: Map<string, number>;
    }
  >();

  /**
   * 統計を記録
   */
  record(
    endpoint: string,
    retryCount: number,
    totalDelay: number,
    errorType: string
  ): void {
    if (!this.stats.has(endpoint)) {
      this.stats.set(endpoint, {
        totalCalls: 0,
        totalRetries: 0,
        totalDelay: 0,
        errorCounts: new Map(),
      });
    }

    const stat = this.stats.get(endpoint)!;
    stat.totalCalls++;
    stat.totalRetries += retryCount;
    stat.totalDelay += totalDelay;

    const errorCount = stat.errorCounts.get(errorType) || 0;
    stat.errorCounts.set(errorType, errorCount + 1);
  }

  /**
   * 統計をレポート
   */
  report(): void {
    this.stats.forEach((stat, endpoint) => {
      const avgRetries =
        stat.totalRetries / stat.totalCalls;
      const avgDelay = stat.totalDelay / stat.totalCalls;

      console.log({
        endpoint,
        totalCalls: stat.totalCalls,
        averageRetries: avgRetries.toFixed(2),
        averageDelay: `${avgDelay.toFixed(0)}ms`,
        errorDistribution: Object.fromEntries(
          stat.errorCounts
        ),
      });
    });
  }
}

// グローバルインスタンス
const retryStats = new RetryStatistics();

定期的に統計をレポートすることで、API の健全性を把握できます。

サーキットブレーカーパターンの追加

連続して失敗が続く場合、一時的にリクエストを停止するサーキットブレーカーパターンも有効です。

typescript/**
 * サーキットブレーカーの状態
 */
enum CircuitState {
  CLOSED = 'CLOSED', // 正常
  OPEN = 'OPEN', // 遮断中
  HALF_OPEN = 'HALF_OPEN', // 試験的に許可
}

/**
 * サーキットブレーカー設定
 */
interface CircuitBreakerConfig {
  failureThreshold: number; // 失敗回数のしきい値
  resetTimeout: number; // リセットまでの時間(ms)
  monitoringPeriod: number; // 監視期間(ms)
}

サーキットブレーカーの実装を行います。

typescript/**
 * サーキットブレーカーの実装
 */
class CircuitBreaker {
  private state: CircuitState = CircuitState.CLOSED;
  private failureCount = 0;
  private lastFailureTime = 0;
  private nextAttemptTime = 0;

  constructor(private config: CircuitBreakerConfig) {}

  /**
   * リクエスト実行可否を判定
   */
  canAttempt(): boolean {
    const now = Date.now();

    if (this.state === CircuitState.CLOSED) {
      return true;
    }

    if (this.state === CircuitState.OPEN) {
      // リセット時間が経過したか確認
      if (now >= this.nextAttemptTime) {
        this.state = CircuitState.HALF_OPEN;
        return true;
      }
      return false;
    }

    // HALF_OPEN の場合は1回だけ試す
    return true;
  }

  /**
   * 成功を記録
   */
  recordSuccess(): void {
    this.failureCount = 0;
    this.state = CircuitState.CLOSED;
  }

  /**
   * 失敗を記録
   */
  recordFailure(): void {
    this.failureCount++;
    this.lastFailureTime = Date.now();

    if (this.failureCount >= this.config.failureThreshold) {
      this.state = CircuitState.OPEN;
      this.nextAttemptTime =
        this.lastFailureTime + this.config.resetTimeout;

      console.error(
        `Circuit breaker opened. Next attempt at ${new Date(
          this.nextAttemptTime
        )}`
      );
    }
  }
}

サーキットブレーカーを組み込んだフェッチ関数です。

typescript/**
 * サーキットブレーカー付きフェッチ
 */
const circuitBreaker = new CircuitBreaker({
  failureThreshold: 5,
  resetTimeout: 60000,
  monitoringPeriod: 300000,
});

async function fetchWithCircuitBreaker(
  url: string,
  options: RequestInit = {},
  config: Partial<RetryConfig> = {}
): Promise<Response> {
  // サーキットが開いていないかチェック
  if (!circuitBreaker.canAttempt()) {
    throw new Error(
      'Circuit breaker is OPEN. Request blocked.'
    );
  }

  try {
    const response = await fetchWithRetry(
      url,
      options,
      config
    );
    circuitBreaker.recordSuccess();
    return response;
  } catch (error) {
    circuitBreaker.recordFailure();
    throw error;
  }
}

サーキットブレーカーにより、障害が長引いている場合に無駄なリトライを避けられます。

まとめ

Dify で API コール失敗に対応するための再試行とバックオフ戦略について解説しました。

重要なポイント

  • エラー種別の理解: 429、5xx、タイムアウトはそれぞれ異なる特性を持つ
  • 適切なバックオフ: Exponential Backoff with Jitter が基本
  • エラーごとの戦略: Retry-After ヘッダーの活用、5xx は早めに諦める
  • 再試行の上限: 無限ループを防ぐため maxRetries を設定
  • 監視とログ: リトライ発生状況を記録し、改善に活かす

実装時の推奨設定

#パラメータ429 エラー5xx エラータイムアウト
1baseDelay2000ms500ms1000ms
2maxDelay120000ms30000ms10000ms
3maxRetries5 回3 回2 回
4timeout60000ms60000ms増加させる

適切な再試行戦略を実装することで、Dify アプリケーションの可用性と信頼性が大きく向上します。

エラーログを定期的に分析し、API プロバイダーのレート制限や障害パターンに合わせて設定を調整していくことが、安定した本番運用の鍵となります。

関連リンク