AI活用

ComfyUIの生成進捗をWebSocketで取得する|Queue・Node・完了通知を理解する

ComfyUIの生成進捗は、HTTPでjobを投入し、WebSocket(/ws?clientId=)でstatus・executing・progress・executedなどのイベントを受け取る二経路で取得します。接続とclientIdの対応、各イベントの読み方、UI stateへの変換、切断時の再確認、進捗率の見せ方を解説します。

この記事の目次
  1. 結論:投入はHTTP、進捗はWebSocket、真実はHTTPで再確認する
  2. なぜHTTPだけでは足りないのか
  3. WebSocketにどう接続するか
  4. 実行イベントをどう読むか
  5. 再現実験:1回の生成で受信イベントを時系列保存する
  6. 現在のNodeをUIにどう渡すか
  7. 切断・再接続にどう備えるか
  8. 進捗率をどう見せるか
  9. よくある質問
  10. clientIdを付けずに接続するとどうなりますか?
  11. バイナリフレームは無視してよいですか?
  12. ブラウザから直接8188のWebSocketにつないでよいですか?
  13. まとめ

ComfyUIの生成進捗は、HTTPのPOST /promptでjobを投入し、WebSocketの/ws?clientId=statusexecutingprogressexecutedといったイベントを受け取る二経路で取得します。投入時のclient_idと接続時のclientIdを同じ値にすると、自分のjobのイベントだけが届きます。接続からイベントの読み方、UI stateへの変換、切断時の再確認、進捗率の見せ方までを整理します。

情報確認日:2026年8月22日(日本時間)

結論:投入はHTTP、進捗はWebSocket、真実はHTTPで再確認する

責任範囲

  • jobの投入と進捗の受信が別経路である理由と、clientIdでのひも付けを説明する
  • 現行のServer API(2026年8月時点)が送るイベント名とpayloadを、公式で確認した範囲で載せる
  • 内部イベントをUI stateへ変換する層の作り方と、切断後に/history/queueで状態を取り戻す手順を示す
  • Nodeごとの所要時間が均等でない前提で、偽の正確さを避ける進捗表示を考える
  • API経路の全体像とHTTPだけの最小実行はComfyUI APIの使い方に任せる
スポンサーリンク

なぜHTTPだけでは足りないのか

POST /promptは、jobをQueueに入れた時点でprompt_idを返して終わり、生成完了を待ちません。HTTPだけで完了を知るならGET /history/{prompt_id}のポーリングになりますが、「今どのNodeか」「samplingが何割か」は分かりません。その途中経過を流しているのがWebSocketです。

  1. 経路1(投入):POST /promptpromptclient_idを送り、prompt_idを受け取る
  2. 経路2(受信):ws://127.0.0.1:8188/ws?clientId=同じ値executingprogressなどを受け取る
  3. 経路3(確定):GET /history/{prompt_id}で出力を確認する

三つ目の経路を残すのが要点です。WebSocketは途中経過の線で、切れても生成は止まりません。「終わったか」「何が出たか」はHTTPの/historyで確かめます。

WebSocketにどう接続するか

server.pyの/wsハンドラは、clientIdクエリがあればそれをセッションID(sid)として使い、なければuuid4().hexを生成します。接続直後に届くstatusメッセージにsidが入っています。

大事なのは、POST /promptclient_idをこのclientIdと同じ値にすることです。execution.pyではexecutingexecutedexecution_errorが投入したクライアントのsid宛てに送られるため、値が違うとjobは動いているのに自分のソケットには何も来ません。

// Node 22 以上(fetch と WebSocket が標準で使える前提)。確認日 2026-08-22
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";

const SERVER = "127.0.0.1:8188";
const clientId = randomUUID();

const ws = new WebSocket(`ws://${SERVER}/ws?clientId=${clientId}`);
await new Promise((ok) => ws.addEventListener("open", ok));

const prompt = JSON.parse(await readFile("workflow_api.json", "utf8"));
const res = await fetch(`http://${SERVER}/prompt`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ prompt, client_id: clientId }),
});
const { prompt_id } = await res.json();
console.log("queued", prompt_id);

順番は「先にWebSocketを開き、次に投入」です。逆だと投入直後のexecution_startを取りこぼし得ます。

実行イベントをどう読むか

公式ドキュメントとソースで確認できるイベントを、1回の生成で届く順に並べます。payloadは{"type": "...", "data": {...}}の形です。

type data の主な項目 意味
status status.exec_info.queue_remaining、初回はsid Queueの残数。接続直後と増減時
execution_start prompt_id そのjobの実行開始
execution_cached prompt_id, nodes 前回と同じ入力で再計算を省いたNodeの一覧
executing node, prompt_id このNodeの処理を開始。nodenullなら全体完了
progress node, prompt_id, value, max Node内の進み(KSamplerなどが報告)
executed node, prompt_id, output UI向け出力(画像など)を返したNodeだけが送る
execution_success prompt_id, timestamp 正常終了
execution_error prompt_idほかNode情報 失敗。どのNodeで何が起きたか
execution_interrupted prompt_id, node_id, node_type, executed /interruptなどで中断

注意点が三つあります。executedはNodeが終わるたびではなく、UIに返す出力を持つNodeだけが送るので、Nodeの切り替わりはexecutingで追います。progressvaluemaxが何を数えるかはNode次第で、全Nodeが送るわけではありません。そしてバイナリフレームが混ざります。公式サンプルは「先頭8バイトがメタ、以降が画像」と説明するsampling途中のpreviewで、JSON.parseの前に文字列判定が必要です。

ソース上にはprogress_state(全Nodeのvaluemaxstateをまとめたもの)もありますが、公式の一覧ページに記載がないため、上の表のイベントだけで動く設計にしておきます。

再現実験:1回の生成で受信イベントを時系列保存する

自分の環境で何がどの順で届くかを記録します。件数や間隔は環境依存なので、手順と表だけを示します。

// 上のコードの続き。受信したものを JSONL に追記する
import { appendFile } from "node:fs/promises";

ws.addEventListener("message", async (ev) => {
  const t = new Date().toISOString();
  if (typeof ev.data !== "string") {
    await appendFile("events.jsonl", JSON.stringify({ t, type: "binary" }) + "\n");
    return;
  }
  const msg = JSON.parse(ev.data);
  await appendFile("events.jsonl", JSON.stringify({ t, ...msg }) + "\n");
  if (msg.type === "executing" && msg.data.node === null && msg.data.prompt_id === prompt_id) {
    ws.close();
  }
});
時刻 type node value / max 気づき
1 status queue_remaining は?
2
最後 executing null 完了までの総時間

表を埋めると、progressを送るNodeがどれか、Nodeごとの所要時間の差、バイナリの件数が自分の環境で分かります。この時間の偏りが最後の節につながります。

現在のNodeをUIにどう渡すか

受信イベントをそのまま画面のstateにしてはいけません。イベント名はServer APIの内部仕様で本体更新で変わり得るからです。「イベント→UI state」の変換層を一つ置き、UI側はその層が出す自分の型だけを見ます。

// progress-state.ts — WebSocketの型名を知っているのはこのファイルだけ
export type JobView =
  | { phase: "queued"; ahead: number }
  | { phase: "running"; nodeId: string; nodeProgress?: { value: number; max: number } }
  | { phase: "done" }
  | { phase: "error"; detail: string };

export function reduce(prev: JobView, msg: { type: string; data: any }, myId: string): JobView {
  const d = msg.data;
  if (msg.type === "status" && prev.phase === "queued")
    return { phase: "queued", ahead: d.status.exec_info.queue_remaining };
  if (d?.prompt_id !== myId) return prev;
  if (msg.type === "executing")
    return d.node === null ? { phase: "done" } : { phase: "running", nodeId: d.node };
  if (msg.type === "progress" && prev.phase === "running")
    return { ...prev, nodeProgress: { value: d.value, max: d.max } };
  if (msg.type === "execution_error") return { phase: "error", detail: JSON.stringify(d) };
  return prev;
}

Node IDをそのまま画面に出しても意味が伝わらないので、API形式JSONのclass_typenodeIdから引ける辞書をWorkflow読み込み時に作っておきます。画面の実装は別記事で扱います。

切断・再接続にどう備えるか

WebSocketは、タブ切り替え、スリープ、プロキシのタイムアウトで静かに切れます。切れている間も生成は続き、再接続しても「その間のイベント」は戻りません。WebSocketだけを真実源にすると画面は「実行中」のまま止まります。

  1. 1
    切断を検知する

    closeerrorイベントに加え、一定時間イベントが来なければ疑う

  2. 2
    同じclientIdで再接続する

    server.pyは同じsidの古いソケットを捨てて置き換える。以後のイベントは再び届く

  3. 3
    HTTPで状態を取り戻す

    GET /history/{prompt_id}outputsがあれば完了。なければGET /queueで実行中・待機中のどちらにいるかを見る

  4. 4
    UI stateを上書きする

    HTTPの結果を正としてJobViewを作り直し、WebSocketは以後の差分にだけ使う

完了の最終判定を/historyにしておけば、WebSocketが一度も届かなくても結果は取れ、落ちても機能は壊れません。Queueの並列数やキャンセルは別記事で扱います。

進捗率をどう見せるか

「全体の何%」を一本の数字で出したくなりますが、Nodeごとの所要時間が均等でないため素直に計算すると嘘になります。checkpointの読み込みは初回だけ長く、KSamplerはstep数に比例し、VAEDecodeは一瞬です。「完了Node数÷総Node数」では、samplingの前に90%に達してそこで止まります。

見せ方 根拠にする情報 向き不向き
段階表示 executingのNode名 嘘がない。「読み込み中→生成中→保存中」の3段階で十分なことが多い
Node内の進捗バー progressvalue / max samplerの間だけ正確。送らないNodeでは出さない
全体% 過去の実行時間の配分 同じWorkflowを繰り返す場合のみ。初回や構成変更後はずれる

段階表示を土台にし、progressが届いている間だけNode内バーを重ねる形を勧めます。全体%を出すなら、実験で記録したNodeごとの所要時間から重みを作り「前回の実行を基にした推定」と明記します。90%で止まるバーより「生成中(12/20)」のほうが状況は正しく伝わります。

よくある質問

clientIdを付けずに接続するとどうなりますか?

サーバーがsidを生成しstatusで返しますが、その値をPOST /promptclient_idに入れない限り自分のjobのイベントは届きません。先に自分でUUIDを決めて両方に使うのが簡単です。

バイナリフレームは無視してよいですか?

preview画像が不要なら無視して構いません。ただしJSON.parseに渡すと例外になるので文字列判定は必ず入れます。

ブラウザから直接8188のWebSocketにつないでよいですか?

自分のPC内なら問題ありません。他の端末や外部に公開する場合は認証のないServer APIを晒すことになるため、認証とプロキシの設計が先です。その設計は別記事で扱います。

まとめ

ComfyUIの進捗取得は、POST /promptで投入し、同じclientIdで開いたWebSocketからexecutingprogressexecutedを受け取り、完了はexecutingnode: null/historyで確定する、という三経路で成り立ちます。

イベント名はUIに直接持ち込まず変換層を挟み、切断後は/history/queueで状態を取り戻し、進捗率は段階表示を土台にしてください。HTTPだけの最小実行はComfyUI APIの使い方を参照してください。

スポンサーリンク