新規登録でプレミアムプランが割引に割引を受け取る

ブログ/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つのルール:

  1. 2xxをすぐに返し、処理はあとで。受領を伝え、キューに入れ、返す。サムネイルをその場でレンダリングするようなハンドラーは、いずれタイムアウトし、配信が失敗したように見えてしまいます。
  2. 配信は「少なくとも1回」として扱う。処理をジョブIDに紐づけ、べき等にしましょう。
  3. 最低限の保険としてポーリングを残す。データベース上でまだ処理中とされているジョブを定期的に確認すれば、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にあります。

タグvideo-apiqueueswebhooksproductionscaling

この記事で使ったモデルで作ってみましょう。無料の50クレジットで始めるか、全エンジンと料金をご覧ください。