Dify APIで「504 Gateway Timeout」が発生する原因と解決策

Difyタイムアウト

Dify APIで「504 Gateway Timeout」が発生する原因と解決策

はじめに

Dify のワークフローAPIを使ったアプリケーションを運用していたところ、間欠的に謎のエラーが発生するようになりました。

エラーメッセージは「Failed to get response from AI workflow」。しかし、Difyの管理画面を確認すると、すべての実行がSUCCESSになっているという不思議な状況でした。

この記事では、この問題の原因と解決策を共有します。

症状

  • ワークフローを実行すると、約60秒後にエラーが返ってくる
  • エラーメッセージ: 「Failed to get response from AI workflow」
  • Dify管理画面では実行成功(SUCCESS)と表示される
  • 毎回ではなく、処理時間が長いときだけ発生

原因の調査

デバッグコードの追加

まず、PHPのcURL呼び出し部分にデバッグコードを追加して、実際のエラー内容を確認しました。

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($httpCode >= 400) {
    error_log("HTTP Error: $httpCode");
    error_log("Response: " . substr($response, 0, 500));
}

判明した事実

レスポンスを確認すると、以下のようなHTMLが返ってきていました:

<html>
<head><title>504 Gateway Time-out</title></head>
<body>
<center><h1>504 Gateway Time-out</h1></center>
</body>
</html>

504 Gateway Timeout でした!

根本原因

アーキテクチャの問題

全体のリクエストフローは次のようになっていました。

リクエストフローの全体像
ブラウザ (タイムアウト: 5分)
    ↓
PHPサーバー (タイムアウト: 5分)
    ↓
nginx / ロードバランサー / Cloudflare (タイムアウト: 約60秒) ← ★ここ!
    ↓
Dify API (処理完了まで60〜90秒)

何が起きていたか

  1. PHPが blocking モードでDify APIを呼び出す
  2. Difyはワークフローを実行(60〜90秒かかる)
  3. 60秒経過時点で、中間のnginx/ロードバランサーがタイムアウト
  4. Difyは処理を完了するが、レスポンスは途中で切断される
  5. PHPは504エラーを受け取り、エラーレスポンスを返す
  6. Dify管理画面では「成功」と表示される(実際に処理は完了しているため)

Dify公式ドキュメントの記載

Difyの公式ドキュメントにも以下の記載があります:

“Cloudflare timeout is 100s for blocking”

実際には、nginx や各種ロードバランサーのデフォルト設定は60秒程度のことが多いです。

Difyタイムアウト

解決策

blockingモードからstreamingモードへ変更

Dify APIには2つのレスポンスモードがあります:

モード 説明
blocking 処理完了まで待ってから、一括でレスポンスを返す
streaming 処理中も逐次データを送信する(Server-Sent Events形式)

streamingモードでは、処理中も定期的にデータが送信されるため、接続がアイドル状態にならず、タイムアウトしにくくなります。

実装方法

方法1: フロントエンドでストリーミング処理(推奨)

フロントエンドでEventSourceや fetch + ReadableStream を使ってストリーミングを処理する方法です。リアルタイムで進捗を表示できます。

方法2: バックエンドで内部処理(今回採用)

既存のフロントエンドを変更せずに対応したい場合、バックエンド(PHP)でストリーミングを内部処理し、最終結果だけをJSONで返す方法があります。

以下は方法2の実装例です。

PHPコード例

<?php
// Dify API設定
$config = [
    'api_url' => 'https://your-dify-instance.com/v1/workflows/run',
    'api_key' => 'your-api-key',
    'timeout' => 300,
];

// ワークフロー実行
function callDifyWorkflow($config, $inputs, $userId) {
    $ch = curl_init($config['api_url']);

    $payload = [
        'inputs' => $inputs,
        'response_mode' => 'streaming',  // ★ ここがポイント!
        'user' => $userId
    ];

    $headers = [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $config['api_key']
    ];

    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => false,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => json_encode($payload),
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_TIMEOUT => $config['timeout'],
        CURLOPT_CONNECTTIMEOUT => 30,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_FOLLOWLOCATION => true,
    ]);

    // ストリーミングデータを内部で処理
    $buffer = '';
    $finalResult = null;

    curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($curl, $data) use (&$buffer, &$finalResult) {
        $buffer .= $data;

        // 改行で区切られた各行を処理
        while (($pos = strpos($buffer, "\n")) !== false) {
            $line = substr($buffer, 0, $pos);
            $buffer = substr($buffer, $pos + 1);

            // "data: " で始まる行を解析
            if (strpos($line, 'data: ') === 0) {
                $jsonStr = substr($line, 6);
                $event = json_decode($jsonStr, true);

                // workflow_finished イベントから最終結果を取得
                if ($event && $event['event'] === 'workflow_finished') {
                    $finalResult = $event['data']['outputs'] ?? null;
                }
            }
        }

        return strlen($data); // 必ずデータ長を返す
    });

    curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode >= 400 || $finalResult === null) {
        return ['success' => false, 'error' => 'Workflow execution failed'];
    }

    return ['success' => true, 'result' => $finalResult];
}

// 使用例
$inputs = [
    'input_text' => 'ここに入力テキスト',
];

$result = callDifyWorkflow($config, $inputs, 'user_123');

header('Content-Type: application/json');
echo json_encode($result);

ストリーミングイベントの種類

Difyのストリーミングモードでは、以下のイベントが送信されます:

イベント 説明
workflow_started ワークフロー開始
node_started ノード処理開始
node_finished ノード処理完了
workflow_finished ワークフロー完了(★最終結果はここに含まれる)

なぜstreamingで解決するのか

blockingモードとstreamingモードの違い
【blockingモードの場合】
PHP → nginx → Dify
     (60秒間データなし = タイムアウト)

【streamingモードの場合】
PHP → nginx → Dify
     ← node_started (10秒後)
     ← node_finished (30秒後)
     ← node_started (31秒後)
     ...
     ← workflow_finished (90秒後)

データが流れ続けるので、接続が維持される!

streamingモードではこのように一定間隔でデータが流れ続けるため、HTTP接続がアイドル状態にならず、中間のnginxやロードバランサーでのタイムアウトを回避できます。

その他の解決策

nginxのタイムアウト延長

サーバー設定を変更できる場合は、nginxのタイムアウトを延長する方法もあります。

location /api/ {
    proxy_pass http://backend;
    proxy_read_timeout 300;
    proxy_connect_timeout 300;
    proxy_send_timeout 300;
}

ただし、この方法は次のような制約があります。

  • サーバー管理権限が必要
  • 他のサービスに影響する可能性がある
  • Cloudflareなど外部サービスのタイムアウトは変更できない

LLMモデルと処理時間

今回の問題は、使用するLLMモデルによって処理時間が変わることも関係していました。

モデル 処理時間(目安) タイムアウトリスク
GPT-4o-mini 20〜40秒
GPT-4o 30〜60秒
GPT-5-nano 60〜90秒
GPT-5-mini 60〜120秒

新しいモデルや、複雑なワークフローを使う場合は、処理時間が60秒を超えることを想定して、最初から streaming モードを使うことをおすすめします。

まとめ

問題

  • Dify APIで間欠的にエラーが発生
  • Dify管理画面ではSUCCESSなのに、クライアントにはエラーが返る

原因

blocking モードで60秒以上の処理を行うと、中間のnginx/ロードバランサーがタイムアウトしてしまうことが原因でした。

解決策

  • response_modestreaming に変更
  • PHP内部でストリーミングを処理し、最終結果だけを返す

教訓

  1. Dify管理画面のSUCCESSを信じてはいけない(クライアントに届いているとは限らない)
  2. 60秒を超える可能性がある処理はstreamingモードを使う
  3. エラー発生時は、HTTPステータスコードとレスポンス内容を必ず確認する

この記事が同じ問題で悩んでいる方の参考になれば幸いです!

Share the Post: