RunPod Serverless APIの使い方|ComfyUIのジョブをcurlで投げて受け取るまで

生成AI

⚠️ 注意: AI画像生成時は著作権・肖像権にご注意ください。商用利用前には各サービスの利用規約をご確認ください。当ブログは生成された画像に関する責任を負いかねます。

📝 本記事にはアフィリエイトリンクが含まれています。

要約

RunPod ServerlessでComfyUIのエンドポイントを作ったあと、実際にどう叩けば生成物が手元に届くのかをまとめました。

自作のACE-Step 1.5 XLテンプレートをRunPod Hubから立てて、curlで曲を生成し、mp3として保存するまでを実機で通した記録です。ジョブを投げてから受け取るまでは3段階あって、/runsync で1発、とはいきませんでした。

同じHubのlistingからPodとしても立てられるんですが、そちらではComfyUIの画面が開きません。 その理由と、代わりに何ができるのかも書いています。

はじめに

RunPod Serverlessのエンドポイントを作るところまでは、Hubのlistingから数クリックで終わります。問題はその後で、「で、これどうやって使うの?」となりました。

ドキュメントには /run/runsync の説明があります。ただ、実際に投げてみると初回は必ず IN_QUEUE が返ってきますし、生成物はファイルではなくbase64の文字列で返ってきます。このあたりは自分で一度通してみないと分かりませんでした。

この記事では、RunPod ServerlessのComfyUIエンドポイントに対して、

  • ワークフローをどの形式で投げるのか
  • ジョブの完了をどう待つのか
  • 生成された音声をどう手元に落とすのか

を順に書きます。実際に出たエラーも全部載せました。推測ではなく、2026年9月18日に実機で出たものだけです。

使っているのは僕が公開しているACE-Step 1.5 XLのテンプレートですが、手順自体はComfyUI系のServerlessなら共通です。Hubに載せるまでの話はRunPod Hubにテンプレートを公開するまでに書いています。

この記事はAPIの叩き方そのものを扱います。叩いた先をアプリとして仕立てる話は、ComfyUIをRunPodサーバーレスで動かすDiscord botのほうにまとめています。

RunPod Serverless APIの使い方は3段階になる

まず全体像です。「ワークフローを投げたら音が返ってくる」ではありませんでした。

段階 やること
1 /run にワークフローをPOSTしてジョブIDを受け取る
2 /status/<id> を叩いて COMPLETED になるまで待つ
3 output.files[0].data をbase64デコードしてmp3として保存する

/runsync を使えば1リクエストで完結します。ただし同期で待てるのは90秒程度までで、それを超えると IN_QUEUE を返して裏で実行を続けます。初回はワーカーの起動から始まるので、まず超えます。

結局 /status を叩くことになるので、最初から /run/status の形で組んでおくほうが素直でした。

モチベル
/runsync があるなら、そっちのほうが簡単そうに見えますけどね
クーラット
温まってるワーカーに投げるなら実際そうなんです。ただ、最初の1回は絶対に待たされるので、そこで詰まるんですよね

準備:ワークフローをAPI形式で書き出す

投げるJSONの形はこうです。

{"input": {"workflow": <ComfyUIのAPI形式のワークフロー>}}

⚠️ API形式でないと通りません。 ComfyUIの Workflow → Export (API) で書き出したものを使います。

普段ワークフローを保存したときに出てくるJSON(UI形式)とは別物です。UI形式にはノードの座標やリンクの情報が入っていて、API形式はノードIDと入力値だけのフラットな構造になっています。見た目が似ているので、間違えたまま投げてしまいがちなところです。

まずはhealth_checkで生存確認する

いきなり生成を投げる前に、エンドポイントが生きているかを確かめます。

curl -s -X POST "https://api.runpod.ai/v2/ENDPOINT_ID/runsync" \
  -H "Authorization: Bearer API_KEY" -H "Content-Type: application/json" \
  -d '{"input":{"health_check":true}}'
RunPodのエンドポイント概要画面。Quick startにcURLでの/runsync呼び出し例が表示されている
エンドポイント画面のQuick startに、そのまま使えるcURLの例が出ている

ComfyUIの /system_stats の内容が返ってきます。モデルが揃っていることの確認にもなりますし、生成を回さないぶんコストも低いので、この順番をおすすめします。

APIキーの打ち間違いやRead専用キーだと、ここで 401 が返ります。ポーリングのループを組んでから気づくと空回りするので、先に1回叩いておくと無駄がありません。

/runでジョブを投げて/statusで待つ|APIの基本形

/run はジョブIDだけを返します。

{"id":"85979b14-e91a-4781-bbe7-fb70acf0be02-e1","status":"IN_QUEUE"}

ポーリングを回すと、こう進みます。

job=85979b14-e91a-4781-bbe7-fb70acf0be02-e1
1 IN_QUEUE
2 COMPLETED
saved D:/work/base_test_00001.mp3 289069 bytes

2回目のポーリングで完了しました。5秒間隔で回しているので、投げてから10秒ほどです(ワーカーが温まっていた場合)。

このIDを使って /status/<id> を繰り返し叩き、statusCOMPLETED になるのを待ちます。FAILED で終わることもあるので、両方を抜け条件にしておきます。

完了したレスポンスには、生成にかかった時間も入っています。実測はこうでした。

項目
delayTime(ワーカーがジョブを掴むまで) 44.1秒
executionTime(生成そのもの) 14.4秒
出力 mp3 / 289,069バイト

生成条件は xl_base / 50 steps / cfg 6 / 尺10秒 / bpm 72です(エンドポイント 7a084v50oag0rs、2026年9月18日)。

44秒のほうはコールドスタートです。 エンドポイントの既定は Max Workers 1 / Min 0 / idleTimeout 5秒 で、5秒アイドルするとワーカーが落ちます。 次のジョブはまた起動からになるので、続けて試すときは間を空けないほうが速く終わります。

GPUプールは広めにしておく
このlistingでは hub.json でGPUを4種類(AMPERE_24 / ADA_24 / AMPERE_48 / ADA_48_PRO)指定しています。1種類だけに絞ると、在庫の波で image pull: pending のまま進まなくなることがありました。Hub経由でもこの指定は効いています。

base64をデコードしてmp3にする

生成物はファイルのURLではなく、base64の文字列で返ってきます。

output.files[0].filename  … ファイル名
output.files[0].data      … base64

これをデコードして書き出せば、手元でそのまま再生できるmp3になります。

⚠️ COMFY_MAX_INLINE_BYTES(既定48MiB)を超える出力は、base64で返らずファイル名とサイズだけになります。 今回の10秒のmp3は289KB、120秒の曲でも3.5MB程度なので音声では当たりませんが、知らないと「なぜ data が無いのか」で止まります。

投入から保存までのコマンド

分解して説明してきましたが、動く形も置いておきます。

K=YOUR_API_KEY; E=ENDPOINT_ID; D="D:/work"; J=$(curl -s -X POST "https://api.runpod.ai/v2/$E/run" -H "Authorization: Bearer $K" -H "Content-Type: application/json" --data-binary "@$D/req.json" | python -c "import sys,json;print(json.load(sys.stdin).get('id',''))"); echo "job=$J"; for i in $(seq 1 80); do curl -s "https://api.runpod.ai/v2/$E/status/$J" -H "Authorization: Bearer $K" > "$D/resp.json"; s=$(python -c "import json;print(json.load(open(r'$D/resp.json',encoding='utf-8')).get('status'))"); echo "$i $s"; case "$s" in COMPLETED|FAILED) break;; esac; sleep 5; done; python -c "import json,base64;d=json.load(open(r'$D/resp.json',encoding='utf-8'));f=d['output']['files'][0];b=base64.b64decode(f['data']);p=r'$D/'+f['filename'];open(p,'wb').write(b);print('saved',p,len(b),'bytes')"

⚠️ WindowsのGit Bashで書く場合、/d/... という書き方はcurlにもPythonにも通じません。 どちらもWindows版なので D:/... を使ってください。ここは実際に踏みました。

つまずいたところ

全部、実際に出たものです。

症状 原因
{"status":"IN_QUEUE"} が返る エラーではありません。 コールドスタート中
404 endpoint not found エンドポイントIDの打ち間違い
401 APIキーが未置換、またはRead専用
KeyError: 'output' ジョブ結果の保持期限切れ
value_not_in_list: unet_name 配置されていないモデルを指定した
Required input is missing ワークフローのノード入力が足りない

IN_QUEUE は見た目が不穏ですが、正常な応答です。/runsync の戻りにも id が入っているので、そのまま /status を叩けます。実際、health_check/runsync に投げた1回目はこれが返ってきました。

{"id":"sync-7d57830d-e756-46fc-92f3-da0a24323621-e1","status":"IN_QUEUE"}

同じコマンドをもう一度叩くと、今度は中身が返ります。delayTime が 2183ms、executionTime が 82ms でした。1回目の待ちはワーカーの起動で、処理そのものは一瞬だと分かります。

KeyError: 'output' は、完了したジョブの結果が消えたあとに取りに行くと起きます。中身を見ると理由がはっきりします。

keys: ['status', 'title', 'detail']
{'status': 404, 'title': 'Not Found', 'detail': 'endpoint not found'}

detailendpoint not found なので、エンドポイントIDの打ち間違いと同じ見た目になります。でも原因は別で、こちらは結果の保持期限切れです。 status を先に見るようにして、完了直後に保存してしまえば当たりません。

APIキーの置換忘れは401がひたすら並ぶ
ポーリングのループを Bearer API_KEY のまま回してしまい、401 が40回並びました。ループを組む前に1回だけ叩いて確かめる、を先にやったほうが早いです。

ComfyUIのエラーは親切

value_not_in_list は、実際に置いてあるファイル名を列挙してくれます。

unet_name: 'acestep_v1.5_xl_turbo_bf16.safetensors'
not in ['acestep_v1.5_xl_base_bf16.safetensors']

これを見れば、デプロイ時に選んだモデルとワークフローの指定が噛み合っていないことがすぐ分かります。エラーメッセージを読めば自己解決できる場面でした。

ノードごとの入力名が分からないときは、ComfyUIの /object_info を見るのが確実です。ただし後述のとおりHubのPodでは8188に届かないので、テンプレートから起動したPod(https://POD_ID-8188.proxy.runpod.net/object_info/ノード名)か、手元のComfyUIで引くことになります。

モデルとサンプラーの対応を外すと動かない

デプロイ時に選んだモデルしか入っていない

Hubのデプロイ画面で選んだdiffusion modelだけが配置されます。ワークフローの unet_name はそれに合わせてください。turboを選んだのにbaseを指定する、その逆も同じです。

RunPod Hubのデプロイ設定ダイアログ。プリセットのプルダウンにTurbo・Base・All modelsの3つが並び、その下にDiffusion modelの選択がある
選べるのはdiffusion modelだけ。text encoderの項目は無い

text encoderのほうは選択式ではなく、qwen_0.6bqwen_4b が常に入ります。公式ワークフローが DualCLIPLoader でこの2つを読むためです。選択肢が無いのは意図的で、片方だけでは動かないからです。

turboとbaseはサンプラー設定が別物

steps cfg sampler / scheduler
xl_turbo 8 1 euler / simple
xl_base 50 6 euler / simple

⚠️ unet_name だけ差し替えても噛み合いません。 turboは蒸留モデルなので8 stepsで成立しますが、baseを8 stepsで回すと生成自体は通って、音がぐずぐずになります。 エラーが出ないぶん気づきにくいところです。

モデルごとの違いはACE-Step 1.5 XLの解説記事にまとめています。

Podとして立てるとどうなるか

同じHubのlistingから、ServerlessではなくPodとしても起動できます。ただし想像していたものとは違いました。

HubのPodではComfyUIの画面が開けない

⚠️ HubのDeployでPodを選んでも、ComfyUIの画面は出てきません。

Hubのlistingにはポートを宣言する場所が無く、開くのは 80/http443/tcp だけです。ComfyUIは8188で動いていますが、外に出ていません。

Podの画面のConnectタブを開くと、それがそのまま見えます。

RunPodのPod詳細画面のConnectタブ。HTTP servicesにPort 80のみが並んでいる
HTTP servicesはPort 80だけ。8188はそもそも一覧に無い

起動ログを見ると、これが何なのかが分かります。

[INFO] Serverless runtime is ready
[INFO] Local API is ready  {"url": "http://127.0.0.1:80/v2/LOCAL"}
[INFO] Running container command  {"command": "/opt/runpod/start.sh"}
[start] ACE-Step XL variant: xl_base
[start] text encoders: qwen_0.6b, qwen_4b

サーバーレスのワーカーを、Podとして動かしたものでした。RunPod側のランタイムが先に立ってポート80でジョブAPIを出し、その配下でComfyUIが起動しています。

ログを追うと、その順番がそのまま見えます。

Podの起動ログ。ComfyUIが8188で待ち受けたあとStarting Serverless Workerと出ている
ComfyUIは8188で待ち受けている。そのうえでサーバーレスのワーカーが起動する

ready: ComfyUI will listen on 0.0.0.0:8188 のあとに starting the serverless handler、そして Starting Serverless WorkerComfyUIは確かに8188で動いています。外から届かないだけです。

80番のルートを開いても404です

8188が公開されていない以上、ComfyUIの画面は開けません。では80番を開けばいいのかというと、こちらもルートは404を返します。

ただし意味が違います。 ジョブAPIは /v2/LOCAL の下にあって、ルートには何も置かれていないだけです。ポート自体は開いています。

ブラウザで80番のproxyURLを開いた画面。404 page not foundとだけ表示されている
ポートは開いているが、ルートには何も置かれていない

叩き方はこうなります。

# 生存確認
curl -s -X POST "https://POD_ID-80.proxy.runpod.net/v2/LOCAL/runsync" \
  -H "Content-Type: application/json" -d '{"input":{"health_check":true}}'

# ワーカーの状態
curl -s "https://POD_ID-80.proxy.runpod.net/v2/LOCAL/health"

/health はこう返ってきます。

{"jobs":{"completed":3,"failed":0,"inProgress":0,"inQueue":0,"retried":0},
 "workers":{"idle":0,"initializing":0,"ready":0,"running":0,"throttled":0,"unhealthy":0}}

⚠️ Authorization ヘッダなしで200が返ります。 公式ドキュメントのサンプルには Bearer YOUR_API_KEY が付いていますが、実際には要求されませんでした。上の2つのコマンド、どちらにもヘッダを付けていません。

URLが知られた場合、請求が増えるわけではありません(Podは時間課金なのでジョブ数と無関係です)。増えるのではなく、GPUを使われ、任意のComfyUIワークフローを実行されるのがリスクです。お金の話ではなく、計算資源と実行権限の話として考えるほうが実態に合います。

Podでの実測

項目
GPU NVIDIA L4 22,563MB / $0.49 per hour
データセンター EU-RO-1
image pull 50秒
モデルDL(18.5GiB) 1分50秒
ComfyUI起動 10秒
合計 2分52秒
生成 16.2秒 / 286,765バイト

Pod gny3gmg98lwp1e、2026年9月18日の値です。価格は執筆時点(2026年9月)のもので、GPUの空き状況や単価は変わります。

こちらは /runsync 1回で完結します。 ワーカーが温まったままなので、/status のポーリングが要りません。Serverlessのコールドスタートとの違いがここに出ます。

HTTP 200
status: COMPLETED / exec: 307 ms
saved D:/work/my_base_test_00001.mp3 286765 bytes
同じワークフローを投げ直すと生成が走りません
この exec: 307 ms は、16.2秒かかった生成の直後にまったく同じワークフローを投げ直したときの値です。ComfyUIは同じ入力の結果をキャッシュするので、実際には生成せずに前の結果を返しています。ファイルサイズも286,765バイトで一致しました。条件を変えながら測るときは、seedや尺を変えないと前の結果を見ていることになります。
イメージが軽い=起動が速い、ではない
同じイメージでも、取得元によってpullの時間は変わります。Hubがビルドしたイメージは registry.runpod.net から引くので50秒でしたが、GHCRから引いた別のPodでは4分27秒かかりました。

用途で選び分ける

3つ並べると、それぞれの役割がはっきりします。「PodかServerlessか」の二択で考えると外します。

向く用途 制約
Hub → Serverless 生成サービスとして使う。 待機中は無料、自動でスケールする コールドスタートが数分、APIキーが必要
Hub → Pod ワーカーの動作確認とデバッグ。 ログが読めて、回数を気にせず叩ける 画面は無い、起動に2分52秒
イメージ + 自分のテンプレート ComfyUIを画面で触る。 ワークフローを組む テンプレートを自分で用意する

HubのPodに画面が無いのは、欠点というより位置づけの違いだと思っています。Hubがビルドしたイメージは registry.runpod.net に置かれてHub経由でしか取得できないので、そのイメージの挙動を確かめる手段がHubのPodデプロイなんですよね。ワークフローを組みたいなら、そもそもテンプレートから起動したPodのほうが合っています。

認証の有無は課金モデルと対応している

課金 認証
Serverless リクエストごとにワーカーが起動、秒課金 必要
Pod 起動中ずっと時間課金、ジョブ数と無関係 不要

リクエストが直接お金になる側だけ、認証を要求しているという形です。並べてみると筋が通っていました。

RunPodの課金の考え方はRunPod料金・クレジットガイドにまとめています。

おまけ:出力から生成条件を復元できる

受け取ったmp3を覗いてみたら、ID3タグに生成条件のJSONがそのまま埋まっていました。

ID3はmp3の中にあるタグ領域で、ふだんは曲名やアーティスト名が入っているところです。そこの TXXX(自由記述に使えるフレーム)に、prompt という名前でワークフローが丸ごと書き込まれていました。

ID3v2.4 / タグ長 1,539バイト
TXXX (1,494バイト)
prompt {"1": {"class_type": "UNETLoader", "inputs":
  {"unet_name": "acestep_v1.5_xl_base_bf16.safetensors", ...

289KBのファイルに対して1.5KBなので、容量としてはほぼ誤差です。base64をデコードした先頭にワークフローが見えるのもこれが理由でした。後から「どの設定で作ったんだっけ」を追えるので、条件を変えながら試すときに助かります。

⚠️ 裏を返すと、mp3をそのまま配ると生成条件も一緒に渡るということです。プロンプトを見せたくない場合は、公開前にタグを落としてください。ComfyUIがPNGにワークフローを埋め込むのと同じ話です。


QIN_QUEUEが返ってきたら失敗ですか?
A

失敗ではありません。ワーカーの起動を待っている状態です。レスポンスに含まれる id を使って /status/<id> を叩けば、完了後に結果を取得できます。初回はコールドスタートで必ずこの状態を通ります。

Q/runsyncと/runはどちらを使えばいいですか?
A

最初から /run/status の組み合わせで書いておくのが確実です。/runsync は同期で待てる時間に上限(90秒程度)があり、それを超えると結局 IN_QUEUE を返します。ワーカーが温まっている状態なら /runsync の1回で完結します。

QワークフローのJSONはどれを使えばいいですか?
A

ComfyUIの Workflow → Export (API) で書き出したAPI形式を使ってください。普段の保存で出てくるUI形式は構造が別物で、そのまま投げても読み込めません。

QHubからPodを立てればComfyUIの画面が使えますか?
A

使えません。Hubのlistingではポートを宣言できず、開くのは80番と443番だけなので、8188番のComfyUIは外に出ていません。画面を使いたい場合は、自分のテンプレートからPodを起動してください。

まとめ

RunPod ServerlessのComfyUIをAPIで動かすときのポイントを整理します。

  • 流れは /run/status → base64デコードの3段階。/runsync 1発では終わらない
  • ワークフローは API形式で書き出したものを投げる
  • いきなり生成せず、health_check で生存確認してから進めると無駄がない
  • IN_QUEUE はエラーではなくコールドスタート中。5秒アイドルでワーカーが落ちるので、続けて試すなら間を空けない
  • HubのPodではComfyUIの画面が開かない。 ジョブAPIを80番で受けるワーカーが動いている
  • 認証が要るのはServerlessだけ。 リクエストが直接課金になる側だけが守られている

エンドポイントを作るところまでは簡単でも、実際に生成物を手元に落とすまでには小さな段差がいくつかありました。どれもドキュメントを読むだけでは分からず、一度通してみて初めて見えたものです。

同じ構成を試す場合は、公開しているテンプレートから起動できます。

AIで作った画像集・プロンプトPDF付き BOOTHで販売中

⚠️ AI画像生成をご利用の際の重要な注意事項

著作権・知的財産権について

  • 既存のキャラクター、作品、ブランドロゴなどの模倣・複製は著作権侵害にあたる可能性があります
  • 商用利用時は特に注意が必要です

肖像権について

  • 実在人物(著名人・一般人問わず)の顔や特徴を模倣した画像生成はお控えください
  • 無断での肖像権使用は法的トラブルの原因となります

利用規約の確認

  • 各AI画像生成サービスの利用規約を必ずご確認ください
  • 商用利用の可否、生成画像の権利関係は各サービスで異なります

免責事項

  • 当ブログの情報を参考にしたAI画像生成により生じた問題について、当ブログは一切の責任を負いません
  • 法的問題が生じた場合は、利用者の自己責任となります
  • 最新の法律・規約情報は公式情報をご確認ください

適切なAI画像生成を心がけ、創作活動を楽しみましょう。
詳細についてはAIと著作権についてをご覧ください。

コメント