ブログ記事1万件について、category、tags、summaryをAIで作り直したいとします。

最初の実装はかなり単純です。

for (const article of articles) {
  const result = await classify(article);
  await save(result);
}

数十件や数百件なら、これで十分なことがあります。

問題は、件数が増えたときです。

1件の処理が2秒でも、1万件を完全な逐次処理で流せば単純計算で5時間半以上かかります。その間にプロセスが落ちるかもしれません。並列化すれば速くなりますが、今度はrate limit、retry、DB負荷、memory usageなどを考える必要が出てきます。

ここまで来ると、考える対象は「AIをどう呼ぶか」だけではありません。

大量の外部API requestを、途中で失敗しても復旧できる形でどう運ぶかというジョブ処理の問題になります。

この記事では、次の3つを比較します。

  • 単純な逐次処理
  • Queue + Responses API
  • OpenAI Batch API

大量なら必ずBatchを使う、という話ではありません。即時性や優先度、失敗時の扱いによって、単純なforループの方が適しているケースもあります。

forループそのものが問題なわけではない

最初に分けておきたいのは、forループと大量処理の運用設計です。

次のコードは、1件ずつ処理して結果を保存します。

for (const article of articles) {
  const result = await classify(article);
  await save(result);
}

この方式には分かりやすい利点があります。

  • 1件終わるたびに結果を保存できる
  • 失敗したrequestをその場で扱える
  • 同時実行数を考えなくてよい
  • rate limitへ急激な負荷をかけにくい
  • デバッグしやすい

500件を一度だけ分類する程度なら、処理時間を許容できるのであれば、この構成で終わらせる方が実装も運用も簡単です。

問題になるのは、処理が長時間化したときです。

1件目 完了
2件目 完了
...
4,381件目 完了

Node.js process crash

再起動

何も記録していなければ、どこから再開するのか分かりません。

そこで最低限、処理済みかどうかをDBへ残す必要があります。

pending
processing
completed
failed

この時点で、単純なforループでも小さなジョブ管理が始まっています。

速くするために並列数だけ増やすと別の問題が出る

逐次処理が遅いなら、100件ずつ同時に送ればよいように見えます。

100 requests

Responses API

100 responses

しかし外部APIでは、単純にconcurrencyを上げればよいとは限りません。

100件を同時送信

rate limit

複数requestが失敗

全workerが同じタイミングでretry

さらに負荷が集中

いわゆるretry stormです。

AI APIに限らず、大量の外部API呼び出しでは次のような要素が増えます。

  • 同時実行数
  • rate limit
  • exponential backoff
  • retry回数
  • timeout
  • process shutdown
  • DB書き込み負荷
  • queue backlog
  • dead-letter処理
  • 途中まで終わったjobの再開

「1万件をAIで処理する」という要件でも、難しくなる部分の多くは通常の分散処理やジョブ処理と同じです。

3つの方式を先に比較する

大まかな違いを並べると次のようになります。

観点逐次処理Queue + Responses APIBatch API
実装量少ない多い中程度
即時性1件ずつ結果を得られる高い低い
concurrency制御基本不要自分で管理OpenAI側へ預ける
retry制御自分で実装細かく設計できるBatch後に失敗requestを再投入
priority付けにくい付けやすいBatch内の優先度制御には向かない
process crashへの耐性checkpoint次第Queue次第Batch作成後の実行はOpenAI側
途中結果の利用しやすいしやすいBatch完了後の取得が基本
rate limit通常APIの制約通常APIの制約を自分で管理通常APIとは別pool
API料金通常料金通常料金同期API比50%割引
向く処理少量・一度きり低遅延・継続処理大量・独立・offline

この表だけを見るとBatchが便利に見えますが、Queueが不要になるわけではありません。

それぞれが管理している範囲が違います。

Queue + Responses APIはApplication側でjobを管理する

Queueを使う場合、Application側が各jobの状態を持ちます。

Producer

Queue

Worker × N

Responses API

DB

たとえば1記事を1jobにします。

article-123
status: pending

article-124
status: processing

article-125
status: failed
attempt: 2

Worker数を10にすれば10件ずつ、50にすれば50件ずつ処理できます。

この方式の強みは、制御をApplication側に残せることです。

たとえば次のような要件を持てます。

  • 有料userのjobを優先する
  • 1userがqueueを占有しないようにする
  • 失敗理由によってretry間隔を変える
  • 3回失敗したらdead-letterへ送る
  • userがcancelしたjobを止める
  • AI処理後すぐに別の処理を実行する
  • 進捗を画面へリアルタイム表示する

顧客問い合わせを数秒以内に分類したいような処理では、この柔軟性が必要になります。

一方で、Queue、Worker、retry、rate limit制御まで自分で運用することになります。

Batch APIは大量のrequestをOpenAI側へ預ける

OpenAI Batch APIでは、requestをJSONLへまとめてアップロードし、Batchとして非同期実行します。

Application

JSONL

Batch API

OpenAI側で非同期処理

output / error file

2026年8月29日時点の公式ガイドでは、主に次の仕様が案内されています。

  • 1 Batchあたり最大50,000 request
  • Batch input fileは最大200MB
  • completion windowは24時間
  • Batch用rate limitは通常の同期APIとは別pool
  • Batchは同期APIに比べて50%のcost discount
  • 1時間に最大2,000 Batchを作成可能
  • 成功requestはoutput file、失敗requestはerror fileへ出力
  • outputの行順はinputと一致するとは限らない
  • requestごとの対応には一意なcustom_idを使う

対応endpointには/v1/responses/v1/chat/completions/v1/embeddingsなどがあります。対応modelは広いものの、すべてではないため、利用するmodelのreferenceでBatch対応を確認する必要があります。

Batchは「同期APIを高速化するmode」ではありません。

今すぐresponseが必要ではない大量requestをまとめて預け、後で結果を取得する仕組みです。

1万記事ならJSONLの1行を1記事にできる

たとえば記事ID 123と124を分類するなら、Batch inputは次のようにできます。

{"custom_id":"article-123","method":"POST","url":"/v1/responses","body":{"model":"gpt-5.6-luna","input":"この記事を分類してください。本文: ..."}}
{"custom_id":"article-124","method":"POST","url":"/v1/responses","body":{"model":"gpt-5.6-luna","input":"この記事を分類してください。本文: ..."}}

重要なのはcustom_idです。

ここへ自分のDB record IDを含めておけば、結果の順序に依存せず元データへ戻せます。

article-123

Batch result

articles.id = 123へ保存

公式ガイドでも、output lineの順序はinputと一致しない可能性があるため、custom_idで対応させるよう案内されています。

Batchを作るところまでは通常のAPI callでよい

Node.jsでは、公式SDKを使ってinput fileをuploadし、そのfile IDからBatchを作れます。

import fs from "node:fs";
import OpenAI from "openai";

const openai = new OpenAI();

const inputFile = await openai.files.create({
  file: fs.createReadStream("articles.jsonl"),
  purpose: "batch",
});

const batch = await openai.batches.create({
  input_file_id: inputFile.id,
  endpoint: "/v1/responses",
  completion_window: "24h",
});

console.log(batch.id);

このbatch.idをApplication側へ保存します。

ai_batches

id
batch_id
status
created_at
retrieved_at

ここが復旧時の起点になります。

1件失敗したら1万件全部やり直し、ではない

大量処理で気になるのは部分失敗です。

10,000件

9,999件 success
1件 failed

Batch APIでは、成功したrequestと失敗したrequestを区別できます。

Batch objectにはrequest_countsがあり、total、completed、failedを確認できます。成功したrequestはoutput_file_id、失敗したrequestはerror_file_idから取得できます。

そのため、1件失敗したから1万件すべてを再送する必要はありません。

error file

custom_idを読む

失敗したrecordだけ特定

必要なら新しいBatchへ再投入

ここでもcustom_idが重要です。

Batchを使うなら、inputの行番号やoutputの順序ではなく、最初からApplication側のIDと対応させる方が安全です。

完了後のoutputはFiles APIから取得する

Batchが完了したかは、batch.idからいつでも確認できます。

import OpenAI from "openai";

const openai = new OpenAI();

const batch = await openai.batches.retrieve("batch_abc123");

console.log(batch.status);

完了してoutput_file_idが付いたら、Files APIから内容を取得できます。

if (batch.status === "completed" && batch.output_file_id) {
  const fileResponse = await openai.files.content(batch.output_file_id);
  const output = await fileResponse.text();

  console.log(output);
}

失敗requestがある場合は、同様にerror_file_idを取得します。

公式のFiles API referenceでは、purpose=batchとしてuploadしたinput fileはデフォルトで30日後にexpireするとされています。

一方、2026年8月29日時点で確認できた公開referenceからは、Batchが生成するbatch_outputについて一律の保持期間を明示した記述までは確認できませんでした。

そのため、必要な結果をOpenAI側へ長期間置き続ける前提にはせず、Batch完了後に自分のDBやstorageへ取り込む方がよいでしょう。

自分のサーバーが途中で落ちてもBatch処理は接続に依存しない

Batch APIを使う利点が分かりやすいのは、長時間処理の途中で自分のprocessが停止したときです。

JSONL upload

Batch作成

batch_idをDBへ保存

Application server restart

Batch作成後の処理は、ApplicationがHTTP connectionを維持して待ち続ける方式ではありません。

復旧後に保存済みのbatch_idを読み、状態を問い合わせられます。

server restart

DBから未回収batchを取得

OpenAIへstatus問い合わせ

completedならoutputを取得

DBへ反映

このようなreconciliationを用意しておけば、deployや一時的なnetwork outageがあっても処理を回収できます。

一方、単純なforループでは、processがどこまで進んだかを自分でcheckpointしておく必要があります。

Batchが24時間以内に全部終わらなかった場合

Batchのcompletion windowは、2026年8月29日時点では24hのみです。

24時間以内に処理しきれなかったBatchはexpiredになります。

この場合、未完了requestはcancelされますが、すでに完了したrequestの結果はoutput fileから取得できます。料金も完了した処理分について発生します。

つまり、

expired
=
Batch全体の結果が消える

ではありません。

完了済みと未完了を分け、未完了分だけ再投入できます。

Batchを手動cancelした場合も、すでに完了している結果は返されます。cancel開始後はcancellingとなり、in-flight requestの終了を待ってcancelledへ移ります。

Webhookは便利だが、Batch IDを保存しなくてよい理由にはならない

OpenAIのWebhookには、Batchに関する次のeventがあります。

  • batch.completed
  • batch.failed
  • batch.expired
  • batch.cancelled

そのため、完了をpollingし続けなくても、Webhookをきっかけに結果取得処理を開始できます。

ただし、Batch運用ではWebhookだけを唯一の復旧経路にしない方が設計しやすくなります。

Webhook

「状態が変わった」と知る

batch_id + Batch API

現在状態を確認する

output / error file

結果を回収する

Application側にbatch_idを保存しておけば、Webhookを受け取れなかった場合でも後から状態を確認できます。

Webhook deliveryのretryやduplicate deliveryそのものは別の設計問題なので、ここではBatchの復旧に必要な範囲だけ押さえておきます。

retryは「どの層が行うか」を分ける

大量処理では、「retryする」とだけ書くと設計が曖昧になります。

Queue + Responses APIなら、Application側でrequest単位のretry policyを持てます。

attempt 1
↓ fail
10 sec

attempt 2
↓ fail
60 sec

attempt 3

dead letter

rate limitなら待つ、validation errorなら再実行しない、といった制御もできます。

Batch APIでは、Batchの処理後に失敗requestをerror_file_idから特定し、必要なrequestだけ新しいBatchへ入れ直す構成になります。

Batch A

9,950 success
50 failed

failed 50件を確認

retry可能なものだけBatch B

どちらもretryできますが、retryを管理する場所が違います。

Queueではjob lifecycleをApplication側が持ちます。

BatchではOpenAIへまとめて実行を預け、完了後の失敗処理をApplication側が持ちます。

AI inferenceのretryとside effectのretryは分ける

分類や要約なら、同じrequestをもう一度AIへ送っても、外部システムが壊れることは通常ありません。

しかしAI結果を使って何かを書き換える場合は別です。

AI inference

result

外部システムを更新

たとえば、AI結果を受けてinvoiceを作成する処理をそのままretryすると、二重作成が起きる可能性があります。

そこで、

Batch

inference resultを保存

Applicationがresult IDを確認

side effectを実行

と分離します。

AI requestを再実行してよいかと、外部システムへの更新を再実行してよいかは別の問題です。

Batch APIを使っても、Application側のidempotency設計が不要になるわけではありません。

50%安いからBatchを選ぶ、だけでは決められない

OpenAIの公式ガイドでは、Batch APIは同期APIと比べて50%のcost discountとされています。

またBatch rate limitは通常のper-model rate limitとは別poolです。

大量のoffline処理では、この2点はかなり大きな利点です。

ただし、システム全体のコストはAPI料金だけではありません。

Queue方式なら次の費用があります。

  • Queue infrastructure
  • Worker runtime
  • DB
  • monitoring
  • retryによる追加request
  • 運用時間

Batch方式でも、JSONL生成、結果回収、failed requestの再投入、DB反映などは必要です。

さらに、数秒で結果が必要な処理をBatchへ移すことはできません。

API単価だけならBatchが有利でも、latency要件を満たせなければ選択肢になりません。

モデルごとの具体的な料金やBatch対応状況は変わるため、実装時はmodel referenceも確認します。

QueueとBatchは二者択一ではない

Application QueueとBatch APIを組み合わせる構成もあります。

Application Queue

一定件数を蓄積

JSONLを生成

Batch API

完了

DBへ反映

たとえば大量のjobが継続的に入ってくるものの、各jobは数時間遅れてもよい場合です。

Application Queueは、

  • userごとの受付
  • priority
  • cancel
  • 入力validation
  • job状態管理

を担当します。

OpenAIへのinference部分だけを一定件数ごとにBatch化します。

この構成なら、Batch APIを「Queueの代替」と考える必要はありません。

OpenAIへの大量requestを実行する部分だけ、専用の非同期処理へ預けられます。

ケースA:500件の記事を一度だけ分類する

条件は次の通りです。

500件
一度だけ
数十分かかってもよい
実装に時間をかけたくない

この程度なら、単純な逐次処理でも十分な可能性があります。

重要なのは、途中で落ちたときに再開できることです。

1件ごとにDBへ保存し、未処理recordだけ取得できるなら、大がかりなQueueを導入しなくても運用できます。

ケースB:毎日1万件の商品説明を分類する

毎日1万件
各requestは独立
即時性不要
コストを抑えたい

Batch APIとの相性がよいケースです。

日次処理としてJSONLを作り、Batchを作成し、完了後にcustom_idから商品recordへ戻します。

Application serverが途中で再起動しても、batch_idが残っていれば後から回収できます。

ケースC:SaaSで問い合わせを数秒以内に分類する

継続的にrequestが来る
数秒以内に結果が必要
userごとのpriorityがある
失敗を1件単位で管理したい

これはQueue + Responses APIの方が自然です。

Batchの24時間windowを待つ設計では、必要なlatencyを満たせません。

Worker側でconcurrencyを調整し、必要ならpriority queueやretry policyを持たせます。

ケースD:10万件の既存データを一度だけ移行する

10万件
offline
一度きり
途中で自分のprocessが落ちても継続したい

1 Batchの上限は50,000 requestなので、10万件なら少なくとも複数Batchへ分けます。

たとえば、

Batch A: 50,000
Batch B: 50,000

としてbatch_idを保存しておきます。

このケースでは、Workerを長時間起動し続けずに済み、通常APIとは別rate limit poolを使え、料金も同期APIより低くなるため、Batchの利点が分かりやすくなります。

どれを選ぶかは件数だけでは決まらない

判断するときは、件数より先に次の条件を見ます。

条件向きやすい方式
少量で、処理時間を許容できる逐次処理
実装を最小限にしたい逐次処理
数秒以内に結果が必要Queue + Responses API
userごとのpriorityが必要Queue + Responses API
retry policyを細かく変えたいQueue + Responses API
大量で各requestが独立しているBatch API
数時間遅れても問題ないBatch API
API料金を抑えたいoffline処理Batch API
自分のworkerを長時間維持したくないBatch API
job間に依存関係があるQueue / workflow側で管理

特にBatchへ向いている条件は、

大量・独立・offline・即時性不要

の組み合わせです。

逆に、前のjob結果を見て次の処理内容を変えるworkflowは、そのまま1つのBatchへ詰めるよりApplication側で順序を管理する方が自然です。

まとめ

1万件のAI処理を見て、最初からforループを捨てる必要はありません。

少量で、一度きりで、処理時間を許容できるなら、1件ずつ保存する逐次処理は十分に合理的です。

問題になるのは、件数が増えたときに必要になる運用です。

processが落ちたらどこから再開するか
rate limitをどう扱うか
失敗requestだけ再実行できるか
途中結果をいつ使いたいか
何秒以内に結果が必要か
誰がretryを管理するか

低遅延、priority、細かなretry制御が必要なら、Queue + Responses APIが向いています。

大量の独立requestをofflineで処理できるなら、Batch APIを使うことで、OpenAI側へ非同期実行を預けられます。通常APIとは別のrate limit poolを使え、同期APIより50%低い料金で処理でき、成功・失敗もrequest単位で回収できます。

整理すると次のようになります。

少量 / 実装を簡単にしたい
→ 逐次処理

低遅延 / priority / 細かなretry制御
→ Queue + Responses API

大量 / 独立 / offline / コスト重視
→ Batch API

Batch APIはQueue全体を置き換えるものではありません。

大量の独立したinference requestを、OpenAI側へ非同期jobとして預けるための仕組みとして考えると、使う場所を決めやすくなります。