ブログ/developers·2026年9月3日·2分·執筆:eroqチーム
本番環境でAI動画生成をスケールさせる:キューとリトライ
AI動画を大量に回す方法。非同期ジョブ、自前の永続キュー、レート制限に合わせた同時実行数、失敗時の返金、キーの使い分け、署名付きWebhookまで。
クリップ1本ならデモです。週に1,000本なら運用の問題で、壊れるのはモデルカードに載っている部分ではありません。レンダリングの途中で再起動したプロセス、失敗したジョブの代金を請求された顧客、そして本番サービスに必要だったレート制限の枠を飲み込んでしまったバッチです。
この記事では、大量の処理に耐える動画パイプラインの姿を紹介します。データの形はeroqのもので/docs/videoに記載していますが、考え方はどの非同期メディアAPIにも応用できます。
待たずに送信する
動画のレンダリングには1〜5分かかります。HTTPリクエストをそんなに長く開いたままにすべきではないので、エンドポイントは非同期です。呼び出すと課金されてレンダリングが始まり、すぐに202が返ります。
{
"id": "b7e6c2d4-…",
"object": "video.generation",
"status": "processing",
"created": 1756118400,
"model": "eroq-motion-one",
"duration": "5s",
"poll": "/v1/videos/generations/b7e6c2d4-…",
"usage": { "credits_spent": 100, "credits_remaining": 887 }
}
ほかの何よりも先に、そのIDを永続化してください。呼び出し元に返す前に、ログを書く前に。ジョブIDは、すでに代金を払ったレンダリングを扱う唯一の手がかりです。以下の内容はすべて、IDが再起動するかもしれないマシン上の変数ではなく、データベースに保存されていることを前提にしています。
自前のキュー
キューは必要です。そしてそれは、APIのキューであってはいけません。
ユーザーからの需要には波がありますが、レンダリングの処理能力には波がありません。バッファがなければ、選択肢は悪いものが2つだけです。ピーク時に仕事を断るか、レート制限に拒否されるまで並列リクエストを一斉に投げるか。キューがあれば、どちらもスケジューリングの判断に変わります。
最低限必要なテーブルには、重要な列が5つあります。
- 送信時のレスポンスから得たジョブIDと、最後に確認したステータス
- リクエストのペイロード。再送信がまったく同じレンダリングになるように
- 試行回数。問題のあるジョブが、永遠にではなく3回の試行で止まるように
- そのジョブが属する顧客またはキャンペーン。費用が正しいアカウントに計上されるように
- 消費クレジット。送信時に
usage.credits_spentからコピーしたもの
あとは、一定のレートでキューを消化するワーカーと、まだ処理中のものについてGET /v1/videos/generations/{id}をポーリングする2つ目のループがあれば十分です。
クラッシュ後の復旧には、一覧取得のエンドポイントがあります。GET /v1/videos/generationsは、ワークスペースが過去24時間に送信したすべてのジョブ(チームメンバーのレンダリングも含む)を返し、limitとstatusフィルターが使えます。インラインのメディアは含まれないので、ループで回しても負担がかかりません。
# what is still in flight right now
curl "https://eroq.ai/v1/videos/generations?status=processing&limit=50" \
-H "Authorization: Bearer $EROQ_API_KEY"
データベースとこの一覧が食い違ったら、正しいのは一覧のほうです。
レート制限に合わせて同時実行数を決める
2つの異なる数値があり、それを混同するのがよくある間違いです。
送信レートはAPIによって制限されます。キーごとに1分あたり動画リクエスト6件で、これにプランの倍率が掛かります(Creatorは2×、Studioは4×、Teamは6×、Agencyは8×)。ワーカーの中で、トークンバケットとして実装しましょう。
処理中のレンダリング数を制限するのは、あなたの忍耐とウォレットだけです。それでも明示的に上限を設けてください。処理中の上限があれば、暴走した再試行ループが1か月分のクレジットを午後のうちに使い果たすのを防げますし、アラートを出す基準となる数値にもなります。
バケットは実効上限より少し下に設定し、差はキューに吸収させましょう。429をめったに発生させなければ、その処理はさほど重要ではなくなります。
再試行、返金、そして再試行すべきでないもの
お金の仕組みのおかげで、再試行は安全です。クレジットは送信時に課金され、失敗したレンダリング、または10分以内に完了しなかったレンダリングは自動で返金されます。照合するのは試行回数ではなく、届いたクリップです。
保証はあと2つあります。コンテンツポリシーでブロックされたリクエストにはcontent_blockedが返り、課金されることはありません。そして利用可否のチェックは、引き落としの前に行われます。プランに含まれないモデルには403 plan_requiredが、現在提供できないエンジンには503 model_unavailableが返り、どちらもクレジットが動く前です。どちらの場合も、黙って別のエンジンに切り替わることはありません。手に入るのは、指定したモデルかエラーのどちらかです。出力とログの整合性を保てるのは、この振る舞いだけです。
そうなると、再試行のポリシーは自然に決まります。
- バックオフしながら再試行:
429と、レスポンスがまったく返ってこなかったネットワークレベルの障害。 - 1回再試行し、それでもだめならエンジンを切り替える:
503 model_unavailable。/modelsのラインナップから自分で選んだ代替エンジンのIDを、設定に持っておきましょう。 - 再試行せず、表に出す:
402 insufficient_credits、403 plan_required、403 role_forbidden、そしてcontent_blockedによる拒否すべて。どれも、再試行の時間内に勝手に変わることはありません。 - 失敗したレンダリングの自動再試行は2回まで。2回失敗したプロンプトは、たいてい3回目も失敗します。返金されるのでコストは待ち時間ですが、それこそ顧客が見ているものです。
再送信は古いジョブの復活ではなく、新しいIDを持つ新しいジョブです。テーブルで両方のIDを関連づけておかないと、顧客ごとのコスト計算がずれていきます。
キーは1つではなく、複数を使い分ける
レート制限はキーごとに数えられるので、キーは被害範囲を区切る自然な境界になります。プランには、まさにそのための複数のキーが含まれています。Hobbyは10個、Creatorは20個、Studioは50個、Teamは100個、Agencyは200個です。
妥当な分け方:
- 環境ごとに1つ(本番、ステージング、ローカル)
- 本番ではパイプラインごとに1つ(ユーザーが待っている対話的な処理、夜間のバッチ処理、社内ツールの処理)
- 生成を再販しているなら、大口顧客ごとに1つ。リクエストログで、その顧客の利用状況が読み取れるように
キーは、それを持つメンバーのワークスペースでのロールも引き継ぎます。そのため、退職者の対応はキーの棚卸しではなく、ロールの変更1回で済みます。「メンバー」タブは/dashboard/workspaceにあります。顧客ごとのコスト計算はAI APIのクレジット課金で解説しています。
ポーリングの代わりにWebhookを
1時間に10本のレンダリングならポーリングで問題ありませんが、1,000本なら無駄が大きすぎます。エンドポイントを登録して、代わりにvideo.generation.succeededとvideo.generation.failedを受け取りましょう。
ペイロードは署名付きの小さなエンベロープです。何メガバイトものbase64が、ロードバランサーを通過することはありません。
{
"id": "evt_9f21…",
"type": "video.generation.succeeded",
"created": 1756118400,
"data": {
"id": "b7e6c2d4-…",
"model": "eroq-motion-one",
"duration": "5s",
"content_type": "video/mp4",
"result_url": "https://store.eroq.ai/…/clip.mp4"
}
}
配信にはStripe方式のeroq-signatureヘッダーがつき、形式はt=<unix>,v1=<hex>です。hexは、作成時に一度だけ表示されるエンドポイントのシークレットでtimestamp.bodyを署名したHMAC-SHA256です。1バイトでも信用する前に、定数時間の比較で検証してください。また、数分以上ずれたタイムスタンプは拒否して、リプレイ攻撃を防ぎましょう。
障害を防ぐ、ハンドラーの3つのルール:
- 2xxをすぐに返し、処理はあとで。受領を伝え、キューに入れ、返す。サムネイルをその場でレンダリングするようなハンドラーは、いずれタイムアウトし、配信が失敗したように見えてしまいます。
- 配信は「少なくとも1回」として扱う。処理をジョブIDに紐づけ、べき等にしましょう。
- 最低限の保険としてポーリングを残す。データベース上でまだ処理中とされているジョブを定期的に確認すれば、Webhookが取りこぼしたものを拾えます。また、クリップが保存されずにインラインで返ってきた場合、
result_urlはnullになるので、その分岐も処理してください。
バッチ:シーケンスをリソースとして扱う
単発ではなくシーケンスをレンダリングするなら、12回の送信を自分で取りまとめるのではなく、シーケンスをサーバー側でモデル化しましょう。eroqのfilmsリソースは絵コンテを保存し、POST /v1/films/{id}/renderは1回の呼び出しですべてのシーンを撮影します。シーンは個別に課金され、キューは順番に処理されるので、ウォレットが空になれば実行はきれいに止まります。レスポンスでは、シーンごとにジョブかエラーかが報告されます。詳細は/docs/filmsにあります。
高いエンジンの前に、安いエンジンでパイプラインをテストしましょう。Seedance 1.0 Liteなら5秒で120クレジットなので、エンドツーエンドの予行演習も手頃な費用でできます。キューにはその違いがわかりません。プランごとの利用条件は/pricingにあります。
よくある質問
失敗したAI動画のレンダリングにも料金はかかりますか?
いいえ。クレジットは送信時に課金され、失敗したレンダリング、または10分以内に完了しなかったレンダリングは自動で返金されます。コンテンツポリシーでブロックされたリクエストにはcontent_blockedが返り、課金されることはありません。
サーバーの再起動後、動画のジョブはどうやって復旧すればいいですか?
GET /v1/videos/generationsを呼び出してください。ワークスペースが過去24時間に送信したすべてのジョブが、ステータスとともに一覧表示されます。自分のテーブルと照合し、APIの一覧を正として扱いましょう。
動画生成にはポーリングとWebhookのどちらを使うべきですか?
開発中や処理量が少ないうちは、ポーリングで十分です。常時レンダリングするようになったら、video.generation.succeededとvideo.generation.failedのWebhookに移行しましょう。どちらの場合も、安全網として低頻度のポーリングは残しておいてください。
まずキューを作り、それからパイプラインを組みましょう。エンドポイントのリファレンスは/docs/videoにあります。
この記事で使ったモデルで作ってみましょう。無料の50クレジットで始めるか、全エンジンと料金をご覧ください。