AI活用

OpenAI Batch APIの使い方|JSONLで大量リクエストを非同期処理する

OpenAI Batch APIをJavaScriptで使い、Responses API向けJSONLの生成、upload、24時間枠の非同期処理、custom_idによる順不同結果とエラーの照合まで解説します。

この記事の目次
  1. 結論:入力順を信用せず、custom_idを業務IDへ対応させる
  2. Batch APIが向く処理・向かない処理
  3. 公式上限を設計値として扱う
  4. 10件のJSONL入力を作る
  5. プロジェクトを準備する
  6. 元データとcustom_idを分ける
  7. 送信前にJSONLを検証する
  8. ファイルをuploadしてBatchを作成する
  9. purpose: "batch"で登録する
  10. 24時間のBatchを作る
  11. statusを確認し、完了を待つ
  12. 状態を一つずつ解釈する
  13. 上限付きでpollingする
  14. 結果とエラーをcustom_idで照合する
  15. output JSONLを取得する
  16. 元データへ安全に戻す
  17. 10件の照合表を作る
  18. 失敗行だけを再投入する
  19. コストと運用の注意点
  20. 割引だけで選ばない
  21. 1件あたりの使用量を残す
  22. JSONLを機密情報の集積所にしない
  23. 本番前チェックリスト
  24. まとめ:Batchは「送信」より「照合」が重要

OpenAI Batch APIは、即時応答が不要な大量リクエストをJSONLファイルでまとめ、非同期に処理するAPIです。各行へ一意の custom_id を付け、File APIへ purpose: "batch" でuploadし、Batchを作成します。出力順は入力順とは限らないため、結果は必ず custom_id で元データへ照合します。

OpenAIの公式ガイドでは、Batchは通常の同期APIに対して50%の料金割引、別のレート制限、24時間のcompletion window(完了期限枠)を持つと案内されています。商品説明の一括分類、評価データの生成、Embedding、夜間要約など、今日中に完了すればよい処理に向きます。

この記事では、Responses API向けの10件の入力をJavaScriptでJSONL化し、upload、Batch作成、状態確認、出力・エラーファイル取得、照合、再実行までを実装します。料金や上限は2026年7月20日時点の公式情報を基準にしています。

情報確認日:2026年7月20日(日本時間)

結論:入力順を信用せず、custom_idを業務IDへ対応させる

元データ10件
  ↓ record IDごとにrequestを作る
batch-input.jsonl
  ↓ Files APIへpurpose=batchでupload
Batch作成(endpoint=/v1/responses)
  ↓ statusをpoll
output_file_id / error_file_id
  ↓ JSONLを1行ずつparse
custom_idで元データへ照合
  ↓
成功を保存 / 失敗だけ再投入 / 監査ログを残す

事故を防ぐ5つの原則

  • custom_id はファイル内で一意にする
  • 同じ入力ファイル内では対象endpointとmodelを統一する
  • 機密データをそのままJSONLへ集約しない
  • 出力順ではなく custom_id で保存先を決める
  • 失敗行だけを新しいBatchへ再投入し、成功行を二重処理しない
スポンサーリンク

Batch APIが向く処理・向かない処理

処理 Batch向き 同期API向き
商品・記事の分類 数百〜数万件を夜間処理 保存直後に1件だけ判定
評価データ生成 期限内にまとまればよい 人が画面で対話しながら確認
Embedding 索引更新をまとめて実行 新規文書を即時検索へ反映
チャット 原則不向き 利用者へ数秒以内に返す
障害対応 事後の大量分析 運用者へ即時提案

completion windowは「その時間までに処理する契約上の枠」であり、受付後すぐ終わる保証ではありません。画面で待つ利用者がいる処理や、数分後に価値がなくなる処理には使いません。

公式上限を設計値として扱う

2026年7月20日時点の公式ガイドでは、1つのBatch入力ファイルは最大50,000リクエスト、最大200MBと案内されています。上限ぎりぎりの1ファイルを作るより、業務日、テナント、データ種別などで再実行しやすい単位へ分けます。

上限は公開前に再確認:Batchの対応endpoint、ファイル上限、レート制限、割引、期限は変更される可能性があります。コードの定数と運用手順は、公式ガイドを確認した日付と一緒に管理します。

10件のJSONL入力を作る

プロジェクトを準備する

mkdir openai-batch-sample
cd openai-batch-sample
npm init -y
npm pkg set type=module
npm install openai
export OPENAI_API_KEY="your_api_key_here"
export OPENAI_MODEL="your_available_model_id"

JSONL(JSON Lines)は、1行に1つの完全なJSONオブジェクトを書く形式です。ファイル全体を [...] で囲まず、各行の末尾を改行します。文章中の改行はJSON文字列としてescapeされます。

元データとcustom_idを分ける

import fs from "node:fs";

const model = process.env.OPENAI_MODEL;
if (!model) throw new Error("OPENAI_MODEL is not set");

const records = [
  { id: "article-001", title: "JavaScriptのPromise入門" },
  { id: "article-002", title: "CSS Gridで2カラムを作る" },
  { id: "article-003", title: "WordPressの子テーマとは" },
  { id: "article-004", title: "Node.jsで環境変数を使う" },
  { id: "article-005", title: "Gitのrebaseを安全に理解する" },
  { id: "article-006", title: "TypeScriptの型ガード入門" },
  { id: "article-007", title: "Webアクセシビリティの基本" },
  { id: "article-008", title: "REST APIのエラー設計" },
  { id: "article-009", title: "SQLのインデックスとは" },
  { id: "article-010", title: "MCPのToolとResourceの違い" },
];

const requestRows = records.map((record) => ({
  custom_id: `summary-${record.id}`,
  method: "POST",
  url: "/v1/responses",
  body: {
    model,
    input:
      `記事タイトル「${record.title}」の` +
      "80文字以内の説明文を日本語で1つ作ってください。",
  },
}));

const jsonl =
  requestRows.map((row) => JSON.stringify(row)).join("\n") + "\n";

fs.writeFileSync("batch-input.jsonl", jsonl, "utf8");
console.log(`created ${requestRows.length} requests`);

custom_id はBatch内で一意である必要があります。連番だけでは別の実行と区別できないため、処理種別と自社のrecord IDを含めます。ただし、メールアドレスや氏名など個人情報をcustom IDへ含めないでください。

送信前にJSONLを検証する

const lines = fs
  .readFileSync("batch-input.jsonl", "utf8")
  .trim()
  .split("\n");

const ids = new Set();

for (const [index, line] of lines.entries()) {
  const row = JSON.parse(line);

  if (row.url !== "/v1/responses") {
    throw new Error(`line ${index + 1}: unexpected endpoint`);
  }
  if (row.body.model !== model) {
    throw new Error(`line ${index + 1}: unexpected model`);
  }
  if (ids.has(row.custom_id)) {
    throw new Error(`line ${index + 1}: duplicate custom_id`);
  }

  ids.add(row.custom_id);
}

console.log({ lines: lines.length, uniqueIds: ids.size });

upload後に入力エラーへ気づくと、ファイル作成からやり直しになります。送信前に行数、JSON parse、一意性、endpoint、model、必須フィールド、入力長、ファイルサイズを確認します。

ファイルをuploadしてBatchを作成する

purpose: "batch"で登録する

import OpenAI from "openai";

const openai = new OpenAI();

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

console.log({ inputFileId: inputFile.id });

入力ファイルIDは、元データのsnapshot ID、生成日時、行数、SHA-256と一緒に保存します。後から「どの入力で作った結果か」を再現するためです。

24時間のBatchを作る

const batch = await openai.batches.create({
  input_file_id: inputFile.id,
  endpoint: "/v1/responses",
  completion_window: "24h",
  metadata: {
    job_name: "article-summary-2026-07-20",
  },
});

console.log({
  batchId: batch.id,
  status: batch.status,
});

JSONL各行の url とBatch作成時の endpoint を一致させます。metadata は運用上の検索に使える識別情報です。個人情報や秘密情報は入れません。

statusを確認し、完了を待つ

状態を一つずつ解釈する

status 意味 アプリの対応
validating 入力ファイルを検証中 待機。検証失敗を監視
in_progress 処理中 request countsを進捗表示
finalizing 出力ファイルを作成中 完了まで待つ
completed 処理完了 outputとerror fileを取得
failed 入力検証等で失敗 Batchのerrorsを確認して作り直す
expired 期限までに全件処理できなかった 完了分を保存し、未完了だけ再投入
cancelling / cancelled 取消処理中・取消済み 部分結果と未処理を照合

validatingは入力検証中、finalizingは結果をまとめている状態です。completed 以外でもoutput fileが存在する場合があるため、最終status、request counts、output file、error fileをセットで記録します。

上限付きでpollingする

const terminalStatuses = new Set([
  "completed",
  "failed",
  "expired",
  "cancelled",
]);

async function waitForBatch(batchId) {
  for (let attempt = 1; attempt <= 300; attempt += 1) {
    const current = await openai.batches.retrieve(batchId);

    console.log({
      status: current.status,
      counts: current.request_counts,
    });

    if (terminalStatuses.has(current.status)) return current;
    await new Promise((resolve) => setTimeout(resolve, 60000));
  }

  throw new Error("Polling stopped before terminal status");
}

const finishedBatch = await waitForBatch(batch.id);

60秒ごとに300回はあくまで例です。サーバーレス関数で長時間loopするのではなく、cron、queue、ジョブ管理基盤から定期確認する方が運用しやすい場合があります。画面からのリクエストを開いたまま待たせません。

結果とエラーをcustom_idで照合する

output JSONLを取得する

async function downloadJsonl(fileId) {
  if (!fileId) return [];

  const fileResponse = await openai.files.content(fileId);
  const text = await fileResponse.text();

  return text
    .trim()
    .split("\n")
    .filter(Boolean)
    .map((line) => JSON.parse(line));
}

const outputRows = await downloadJsonl(
  finishedBatch.output_file_id,
);
const errorRows = await downloadJsonl(
  finishedBatch.error_file_id,
);

console.log({
  outputRows: outputRows.length,
  errorRows: errorRows.length,
});

出力ファイルの行順は入力順と一致しません。outputRows[0] を元データ1件目へ保存する実装は誤更新を起こします。

元データへ安全に戻す

const recordByCustomId = new Map(
  records.map((record) => [
    `summary-${record.id}`,
    record,
  ]),
);

const results = [];

for (const row of outputRows) {
  const record = recordByCustomId.get(row.custom_id);
  if (!record) {
    throw new Error(`Unknown custom_id: ${row.custom_id}`);
  }

  if (row.error) {
    results.push({
      recordId: record.id,
      status: "error",
      error: row.error,
    });
    continue;
  }

  const body = row.response?.body;
  results.push({
    recordId: record.id,
    status: "completed",
    responseId: body?.id ?? null,
    summary: body?.output_text ?? "",
    usage: body?.usage ?? null,
  });
}

console.log(results);

Batch行にはHTTP相当のresponseまたはerrorが含まれます。HTTP statusが成功でも、期待する構造や内容が空なら業務上の失敗です。response ID、モデル、usage、出力検証結果を保存します。

10件の照合表を作る

custom_id 元record API結果 保存判断
summary-article-001 article-001 成功 検証後に保存
summary-article-002 article-002 成功 検証後に保存
summary-article-003 article-003 失敗 理由を記録
summary-article-004 article-004 成功 検証後に保存
summary-article-005 article-005 成功 検証後に保存
summary-article-006 article-006 成功 検証後に保存
summary-article-007 article-007 未完了 新Batch候補
summary-article-008 article-008 成功 検証後に保存
summary-article-009 article-009 成功 検証後に保存
summary-article-010 article-010 成功 検証後に保存

この表はエラー・期限切れを含む運用テスト用の期待例です。成功10件だけで試すと、再実行ロジックを検証できません。テスト環境では、未知のcustom ID、重複ID、1件失敗、部分完了、空outputを用意します。

失敗行だけを再投入する

再実行対象は、元の10件から「保存済み成功」を除いて作ります。単に元JSONLをもう一度uploadすると、成功済みの出力を二重保存し、費用も再発生します。

const completedIds = new Set(
  results
    .filter((result) => result.status === "completed")
    .map((result) => `summary-${result.recordId}`),
);

const retryRows = requestRows
  .filter((row) => !completedIds.has(row.custom_id))
  .map((row) => ({
    ...row,
    custom_id: `retry-1-${row.custom_id}`,
  }));

fs.writeFileSync(
  "batch-retry-1.jsonl",
  retryRows
    .map((row) => JSON.stringify(row))
    .join("\n") + "\n",
  "utf8",
);

retry回数をcustom IDやジョブ台帳へ記録し、永続的な入力エラーを無限に再送しないようにします。429や一時障害だけでなく、長すぎる入力、不正なparameter、権限のないモデルなど再送しても直らない原因を分けます。

コストと運用の注意点

割引だけで選ばない

Batchは公式上50%割引ですが、入力ファイル生成、保存、監視、結果照合、失敗再実行の運用コストがあります。件数が少なく即時性が必要なら同期APIの方が単純です。料金管理の考え方はAI APIコスト管理も参照してください。

1件あたりの使用量を残す

全体金額だけでなく、record IDごとに入力・出力トークン、モデル、statusを集計します。想定より長い入力が混ざったときに、どのデータが費用を押し上げたか分かります。料金単価は公式Pricingから実行日時に対応する値を使います。

JSONLを機密情報の集積所にしない

入力ファイルには多数の業務データが1か所へ集まります。生成ディレクトリの権限、暗号化、upload後の削除、ログ、バックアップ、保持期限を決めます。APIへ送ってよいデータか、OpenAIのデータ方針と組織の規程も確認してください。

本番前チェックリスト

  1. 処理は24時間以内の非同期完了で価値を保てる
  2. custom IDが一意で、個人情報を含まない
  3. JSONLの全行をupload前にparse・検証している
  4. endpointとmodelが入力ファイル内で統一されている
  5. 元データsnapshotと入力ファイルのhashを保存している
  6. terminal status、request counts、output/error fileを記録する
  7. 出力順ではなくcustom IDで照合する
  8. 成功・API失敗・内容検証失敗・未完了を分ける
  9. 失敗行だけを上限付きで再投入する
  10. 入力・出力ファイルの保持期限と削除手順がある

まとめ:Batchは「送信」より「照合」が重要

OpenAI Batch APIは、1行1リクエストのJSONLを purpose: "batch" でuploadし、対象endpointと24時間のcompletion windowを指定して作成します。完了後はoutput fileとerror fileを取得し、各行を custom_id で元recordへ戻します。

最初は10件で、順不同、1件失敗、部分完了、未知IDをテストしてください。大量処理へ広げる前に、二重保存を防ぐidempotency(同じ処理を繰り返しても結果を壊さない性質)と、失敗だけを再実行する台帳を完成させることが重要です。

スポンサーリンク