前の記事では、1万件のような大量のAI処理を、単純なforループ、Queue + Responses API、Batch APIのどれで実行するかを扱いました。

今回は件数ではなく、1件の処理が長い場合を考えます。

たとえば、Responses APIへ送った処理が5分かかるとします。

Application
  ↓ request
OpenAI API
  ↓ 5分間処理
Applicationへresponse

この5分の間にApplicationが再起動したら、どうなるでしょうか。

OpenAI側では処理が続いているのか。結果は消えるのか。もう一度同じrequestを送ったら二重実行にならないのか。Reverse Proxyのtimeoutやnetwork interruptionでも同じ問題が起きます。

長い処理を安定して扱うには、HTTP connectionを5分間どう維持するかだけを考えても足りません。

AIの処理とHTTP connectionを別のlifecycleとして扱い、後から状態と結果を取りに行ける形にする必要があります。

OpenAIのResponses APIには、そのためのBackground Modeがあります。ただし、background: trueを付ければ障害対策が完了するわけではありません。response IDをどこへ保存するか、Webhookを取りこぼしたらどうするか、同じ完了通知が2回来たらどうするかまでApplication側で決める必要があります。

この記事では、その部分を中心に見ていきます。

同期処理ではHTTP connectionとAI処理が同じ時間だけ続く

通常のrequestでは、Applicationはresponseが返るまでHTTP connectionを待ちます。

request開始

AI処理

AI処理

AI処理

response

短い処理なら特に問題ありません。

しかし処理時間が数分になると、AIモデル以外の事情で接続が切れる可能性が増えます。

  • Application processの再起動
  • deploy
  • reverse proxyやload balancerのtimeout
  • network interruption
  • client disconnect
  • serverless functionの実行時間制限

ここで厄介なのは、HTTP connectionが切れたことと、OpenAI側の処理が止まったことは同じではないという点です。

Applicationから見るとrequestが失敗したように見えても、OpenAI側ではすでにrequestを受理して処理している可能性があります。

その状態で単純に同じrequestを送り直すと、同じAI処理を2回実行することもあり得ます。

Background Modeでは先にresponse IDを受け取れる

Responses APIではbackground: trueを指定すると、長時間処理をBackground Modeで開始できます。

import OpenAI from "openai";

const openai = new OpenAI();

const response = await openai.responses.create({
  model: "gpt-5.6",
  input: "長い調査レポートを作成してください",
  background: true,
  store: true,
});

console.log(response.id);
console.log(response.status);

概念的には次の流れです。

Application
  ↓ background=true
OpenAI API

response IDを返す

HTTP connection終了

OpenAI側

処理継続

queued

in_progress

completed

Applicationはresponse IDを使って、後から状態を取得できます。

const response = await openai.responses.retrieve("resp_...");

console.log(response.status);

OpenAIの現在のAPI Referenceでは、Responseのstatusとして次の6種類が定義されています。

status意味
queued実行待ち
in_progress処理中
completed完了
failed失敗
cancelledキャンセル済み
incomplete処理は終了したが完全なResponseにならなかった

pollingではqueuedまたはin_progressの間だけ待ち、それ以外へ変わったら最終状態として扱う形が分かりやすいです。

OpenAI公式のBackground Modeガイドでも、queued / in_progressの間はretrieveを繰り返し、終了後のstatusを確認する例が掲載されています。

response IDはApplication memoryだけに置かない

Background Modeを使っても、response IDを変数に持っているだけでは再起動に耐えられません。

request送信

resp_abc123 を取得

Application memoryに保持

process crash

response IDを失う

そのため、response IDを受け取ったら早い段階で永続化します。

たとえば、次のようなrecordを持てます。

ai_jobs

id
openai_response_id
status
created_at
updated_at
completed_at
result_saved_at

Application側のjob IDとOpenAI側のresponse IDを分けて持っておくと、後から追いやすくなります。

ai_jobs.id
job_20260830_001

openai_response_id
resp_abc123

処理開始時の流れは、次のようになります。

自分のDBにjobを作成

OpenAIへbackground request

response ID取得

jobへresponse ID保存

request元へjob IDを返す

ここで保存するstatusはOpenAIのstatusをそのままコピーしてもよいですし、自分のApplication用にsubmittedrunningcompletedなどへ変換しても構いません。

大切なのは、再起動後に「未完了のjobはどれか」をDBから見つけられることです。

再起動後はreconciliationで状態を合わせ直す

Applicationが落ちても、DBにresponse IDが残っていれば復旧できます。

たとえば起動時やcronで、未完了jobを探します。

Application再起動

DBからunfinished jobを取得

openai_response_idを読む

Responses APIへretrieve

最新statusをDBへ反映

このように、自分のDBと外部サービスの状態を後から照合して合わせ直す処理をreconciliationと呼びます。

簡略化すると、次のような実装です。

以下は構成を示す例で、DB部分はApplicationごとに置き換える必要があります。

async function reconcileAiJob(jobId: string) {
  const job = await db.aiJobs.findById(jobId);

  if (!job?.openaiResponseId) {
    return;
  }

  const response = await openai.responses.retrieve(
    job.openaiResponseId,
  );

  await db.aiJobs.update(job.id, {
    status: response.status,
    updatedAt: new Date(),
  });

  if (
    response.status === "completed" &&
    job.resultSavedAt === null
  ) {
    await saveResponseResult(job.id, response);

    await db.aiJobs.update(job.id, {
      resultSavedAt: new Date(),
      completedAt: new Date(),
    });
  }
}

実際にはfailedcancelledincompleteも分けて扱います。

このreconciliationがあると、Webhookを取りこぼしても、Applicationが数分止まっても、DBに残したresponse IDから状態を追い直せます。

Webhookは「完了したら呼んでもらう」仕組み

pollingだけでもBackground Modeは使えます。

every 1 minute

unfinished jobsを取得

Responses APIへretrieve

DB更新

job数が少なければ、この方法はかなり単純です。

一方、jobが増えると、まだ終わっていないResponseへ何度も問い合わせることになります。

そこでWebhookを使うと、OpenAI側で状態が変わったタイミングにApplicationへ通知できます。

OpenAIのWebhookではBackground Responseに対して、response.completedresponse.failedresponse.incompleteresponse.cancelledなどのeventを受け取れます。

OpenAI
  ↓ response.completed
Webhook endpoint

response ID取得

Responses APIからretrieve

DB更新

Webhookのpayloadにはresponse IDが含まれます。完了通知を受け取ったら、そのIDを使って最新のResponseをretrieveできます。

OpenAIの公式例も、response.completedを受けた後にresponses.retrieve()でResponseを取得する形です。

Webhookがあってもpollingを完全には捨てない

Webhookは便利ですが、Webhookだけを唯一の復旧手段にすると別の弱点ができます。

たとえばApplicationが停止している間にWebhookが届けば、その場では受け取れません。

OpenAIの現在の仕様では、Webhook endpointが数秒以内に成功の2xxを返さなかった場合、指数バックオフで最大72時間まで再送されます。

それでも、次のようなケースは残ります。

Application停止が72時間を超えた
endpoint URLの設定を間違えた
署名検証の設定が壊れていた
Application bugでeventを保存できなかった

そのため、Webhookとreconciliationを組み合わせる方が復旧しやすくなります。

通常時
OpenAI
  ↓ Webhook
Application

DB更新

取りこぼし時
cron / startup

unfinished jobs

Responses APIへretrieve

DB更新

Webhookは「状態が変わったので確認してください」というsignalとして使い、DBに残っている未完了jobは別経路でも確認できるようにします。

少量ならpollingだけでも十分

Webhookを使わないと古い設計になるわけではありません。

たとえば同時に数件から数十件しかBackground Responseが動かないサービスなら、1分ごとのpollingだけでも十分なことがあります。

pollingなら、Webhook用に必要になる次の処理がありません。

  • 公開endpointの用意
  • signature verification
  • duplicate deliveryの処理
  • Webhookのlocal development環境
  • event受信用のQueueやtable

一方、job数が増えると、未完了Responseへ問い合わせ続けるAPI callも増えます。

その場合は、

Webhook
+
低頻度のreconciliation

にすると、普段は通知を使いながら、取りこぼしだけ後から直せます。

Webhookは同じeventが複数回来る可能性がある

OpenAIのWebhookガイドには、まれに同じWebhook eventが重複して配信される可能性があると明記されています。

そのため、次のような実装は危険です。

response.completed

メール送信

同じeventが2回来ると、

Webhook #1

メール送信

Webhook #2

メール送信

となる可能性があります。

OpenAIは重複排除にwebhook-id headerをidempotency keyとして使えると案内しています。

たとえば、受信済みWebhookを次のように保存します。

webhook_events

webhook_id
event_type
received_at
processed_at

同じwebhook_idがすでに存在すれば、後から届いた同一eventは処理しません。

Webhook受信

webhook-id確認

未処理なら保存

処理開始

すでに存在

何もしない

DB側でwebhook_idへUNIQUE constraintを付けておくと、Application instanceが複数ある場合にも重複を止めやすくなります。

Webhookのbusiness logicは2xxを返す前に全部実行しない

Webhook endpointで次の処理を全部行う実装も考えられます。

Webhook受信

署名検証

Responses APIへretrieve

DB更新

メール送信

2xx

しかし処理が長くなるほど、OpenAI側のWebhook timeoutに近づきます。

公式ガイドでは、Webhook endpointは速やかに2xxを返し、時間のかかる処理はbackground workerへ渡すことが推奨されています。

実装するなら、次のような形が扱いやすいです。

Webhook受信

署名検証

webhook eventをDB / Queueへ永続化

2xx

別workerがevent処理

ここで順番も重要です。

もし、

Webhook受信

2xx

DB保存

とすると、2xxを返した直後にprocessが落ちた場合、OpenAI側では配信成功になりますが、自分のApplicationにはeventが残らない可能性があります。

そのため、少なくともeventを再処理できる場所へ保存してから2xxを返します。

Webhook署名は検証する

Webhook endpointはInternetから到達できるURLです。

受け取ったJSONをそのまま信頼すると、第三者がOpenAIのWebhookを装ったrequestを送る可能性があります。

OpenAI SDKにはWebhook署名を検証するためのwebhooks.unwrap()が用意されています。

以下は公式ガイドの流れに沿った最小例です。

import OpenAI from "openai";
import express from "express";

const openai = new OpenAI({
  webhookSecret: process.env.OPENAI_WEBHOOK_SECRET,
});

const app = express();

app.use(
  "/webhook",
  express.text({ type: "application/json" }),
);

app.post("/webhook", async (req, res) => {
  try {
    const event = openai.webhooks.unwrap(
      req.body,
      req.headers,
    );
    const webhookId = req.header("webhook-id");

    if (!webhookId) {
      return res.sendStatus(400);
    }

    await persistWebhookEvent({
      webhookId,
      event,
    });

    return res.sendStatus(200);
  } catch (error) {
    if (
      error instanceof OpenAI.InvalidWebhookSignatureError
    ) {
      return res.sendStatus(400);
    }

    console.error(error);
    return res.sendStatus(500);
  }
});

persistWebhookEvent()はApplication固有の処理です。DB側ではwebhook_idをUNIQUEにし、同じeventを二重登録しないようにします。署名不正は400、DB障害などでeventを保存できなかった場合は500を返せば、OpenAI側のretry対象になります。

AI jobの完了と、その後の処理を分ける

AI処理の結果をDBへ保存するだけなら、同じResponseをもう一度retrieveしても大きな問題にならないことがあります。

しかしAI結果をきっかけに別の操作をすると、重複実行が問題になります。

たとえば、

AI処理完了

請求書を作成

や、

AI処理完了

ユーザーへメール送信

です。

Webhookのduplicate deliveryだけでなく、reconciliationも同じcompleted jobを見つける可能性があります。

そこで、AI jobの完了とbusiness side effectを分けます。

OpenAI response.completed

AI resultを保存

job status = completed

side effect用jobを作成

メール送信 / 外部API更新

side effect側には、AI job IDなどを使ったidempotency keyを持たせます。

たとえばメール通知なら、

notification_jobs

id
ai_job_id
type
status
sent_at

UNIQUE(ai_job_id, type)

のようにして、同じAI jobから同じ種類の通知を1回しか作れないようにできます。

Webhook自体が1回だけ届くことを期待するより、同じ処理が再度呼ばれても結果が壊れない形にする方が安全です。

「OpenAIは受理したがresponse IDを受け取れなかった」が最も難しい

Background Modeでも完全には消えない問題があります。

次のケースです。

Application
  ↓ request送信
OpenAI
  ↓ request受理
  ↓ response ID生成
network interruption

Applicationはresponse IDを受け取れない

Applicationから見るとrequestは失敗しています。

しかしOpenAI側では処理が始まっている可能性があります。

ここで同じrequestを再送すると、最初の処理と再送した処理の両方が走る可能性があります。

2026年8月30日時点で確認できるResponses APIの公式ドキュメントでは、この曖昧な状態を一般的なIdempotency-Keyで完全に解消する方法は確認できませんでした。

そのため、少なくともApplication側では次の前提を持つ方がよいです。

  1. request送信結果が不明な場合、AI inferenceが二重実行される可能性を残す
  2. Application側のjob IDをmetadataなどへ入れて追跡しやすくする
  3. AI結果を使ったside effectは必ず重複排除する
  4. 「同じAI処理が2回走ること」と「請求書やメールが2回作られること」を分ける

metadataへApplication側のjob IDを入れる例は次のようになります。

const response = await openai.responses.create({
  model: "gpt-5.6",
  input: "長い処理",
  background: true,
  store: true,
  metadata: {
    job_id: "job_20260830_001",
  },
});

ただし、metadataを入れれば失ったresponse IDを必ず検索して復旧できる、という意味ではありません。

response IDを受け取れなかったrequestは「結果が分からないrequest」として扱い、後段のside effectを安全にする方が現実的です。

retentionを知らないと「後から取れる」が成立しない

Background Responseは永久に残るわけではありません。

OpenAIのData Controlsでは、Responses APIのApplication Stateは原則として30日間保持されます。

ただしBackground Modeには注意点があります。

2026年8月30日時点の公式ドキュメントでは、Background requestでstoreを省略するかfalseにすると、非同期実行とpollingのため一時的にdiskへ保存されますが、その期間はおよそ10分です。temporary polling periodの後はResponseが削除されます。

そのため、長時間後の復旧を想定するなら、保持要件を確認した上でstore: trueを明示する必要があります。

const response = await openai.responses.create({
  model: "gpt-5.6",
  input: "長い処理",
  background: true,
  store: true,
});

Zero Data Retentionが有効な組織ではstoreは常にfalseとして扱われます。Background Modeでも非同期処理とpollingのため約10分は一時保存されますが、通常の30日保存を前提にはできません。

保存期間はデータ要件にも関わるため、実装前に現在のData Controlsを確認した方が安全です。

Background ModeとStreamingは二者択一ではない

Streamingは、tokenやeventを処理途中から受け取るための仕組みです。

Background Modeは、処理そのものをHTTP connectionから切り離して継続できるようにする仕組みです。

役割が違います。

Streaming
処理中のoutputを順次受け取る

Background Mode
接続が切れてもResponseの処理を継続する

現在のResponses APIでは、background: truestream: trueを同時に指定できます。

さらに、Background Responseを最初からstream: trueで作成していれば、streamが途中で切れた後にsequence_numberをcursorとして再開できます。

const stream = await openai.responses.create({
  model: "gpt-5.6",
  input: "長い処理",
  background: true,
  stream: true,
});

ただし、単に完成した結果だけ欲しいのであれば、Background Mode + Webhookまたはpollingの方が実装は単純です。

リアルタイム表示も必要な場合に、Background + Streamingを検討する形で十分でしょう。

Background Modeと自前Queueは役割が違う

Background Modeを使うと、自前Queueが不要になるようにも見えます。

しかしQueueが扱う範囲はもっと広いです。

Application Queue

どのjobをいつ実行するか
priority
rate control
retry schedule
concurrency

Worker

Responses API background=true

Background Modeは、1つのResponses API requestの処理をHTTP connectionから切り離します。

一方、Queueは「どのjobをいつOpenAIへ送るか」を管理します。

そのため、次の構成も普通に成立します。

Producer

Application Queue

Worker

Responses API
background=true

response ID保存

大量のrequestを制御したい場合はQueueが必要になることがありますし、1件の長い処理を接続切断から守りたいだけならBackground Modeだけで足りることもあります。

Batch APIとは対象にしている問題が違う

前の記事で扱ったBatch APIも非同期処理ですが、Background Modeとは目的が違います。

方式向いている処理
Background Mode1件の長時間Response
Batch API大量の独立requestをoffline処理
Streaming生成途中のoutputをすぐ表示
Queuejobの実行順、priority、retry、concurrency管理

たとえばDeep Researchのように1件が数分かかり、完成した結果だけ欲しいならBackground Modeが候補になります。

10万件の文章分類ならBatch APIの方が自然です。

チャットUIでtokenが生成された瞬間から表示したいならStreamingを使います。

処理の優先順位やretry timingまで自分で管理したいならQueueを組み合わせます。

サーバーレス環境では「processが生き続ける前提」を外せる

Cloudflare Workers、AWS Lambda、Vercel Functionsなどでは、1つのprocessを長時間動かし続ける設計は環境ごとの制約を受けやすくなります。

Background Modeなら、Application側はresponse IDを保存した後に処理を終えられます。

HTTP request受信

OpenAI background job作成

response ID保存

Application側のrequest終了

OpenAI側で処理継続

Webhook / polling

結果保存

この構成では、AI処理が5分かかるからApplication processも5分生き続ける、という前提がなくなります。

サーバーレス固有のtimeout値を覚えることより、process lifecycleとAI processing lifecycleを分けられることの方が設計上は大きな利点です。

最小構成から始めるならどうするか

数分かかるAI処理を初めて非同期化するなら、最初から複雑なイベント基盤を作る必要はありません。

job数が少ないなら、次の構成でも十分です。

1. DBにai_jobを作る
2. background=trueでResponses APIを呼ぶ
3. response IDをDBへ保存する
4. 1分ごとにunfinished jobをpollingする
5. completedなら結果を保存する

jobが増えてpolling量が気になってきたらWebhookを追加します。

Webhook

普段の完了検知

reconciliation

取りこぼしの復旧

外部通知や請求処理などが増えたら、AI jobの完了処理とside effectを分けます。

AI job
  ↓ completed
result保存

side effect job

メール / 外部API / 請求処理

この順で足していけば、最初から必要以上に複雑な構成にしなくて済みます。

まとめ

AI処理中にApplicationが落ちても、Background Modeで開始したResponseまで必ず消えるわけではありません。

response IDを永続化していれば、Application再起動後にResponses APIへretrieveして状態と結果を回収できます。

ただし、安定した非同期処理にするにはBackground Modeだけでは足りません。

Background Mode

AI処理をHTTP connectionから切り離す

response ID + DB

再起動後に追跡できる

Webhook

状態変化を早く知る

polling / reconciliation

通知の取りこぼしを復旧する

idempotency

重複eventや再処理でside effectを二重実行しない

OpenAIのWebhookは最大72時間再送されますが、duplicate deliveryもあり得ます。webhook-idによる重複排除と、未完了jobを後から確認するreconciliationを残しておくと、どちらか一方へ依存しすぎずに済みます。

もう一つ注意したいのがretentionです。Background Modeでstoreを省略またはfalseにした場合、後からretrieveできる期間は約10分のtemporary polling periodに限られます。長時間後の復旧が必要なら、データ保持要件とstoreの設定を先に確認しておく必要があります。

AI APIだから特別な障害設計が必要というより、外部の非同期サービスを扱うときと同じです。

「接続が切れないようにする」より、「接続が切れても状態を追い直せる」構成にしておくと、Applicationの再起動やdeployを通常の運用として扱いやすくなります。

参考