Nullstead

OpenAI

OpenAI Responses APIのBackground mode実践運用ガイド:長時間AIジョブを「待てる設計」に変える個人開発ランブック

OpenAI Responses APIのBackground modeを使い、長時間タスクをタイムアウト地獄から救う実践ガイドです。ポーリング、Webhook、再実行、キュー連携まで、個人開発でも壊れにくく回す設計を敬体で解説します。

約12分

はじめに:なぜ今「待てる設計」が必要なのか

AIエージェントを個人開発で回していると、だいたい同じ地雷を踏みます。最初は同期APIでサクッと試して「おお、賢い」と喜ぶのですが、次の瞬間に「処理が長い案件」で詰まります。具体的には、調査系、長文生成、複数ツール連携です。HTTP接続の向こうでモデルは真面目に考えているのに、こちらのサーバーやクライアントが先に息切れするのです。人間で言うと、会議中に回線だけ先に退席する現象です。

そこで効いてくるのが OpenAI Responses API の Background mode です。非同期でジョブを投げ、後から状態を取りに行く。あるいはWebhookで完了通知を受け取る。いわば「その場で待たない勇気」を設計に組み込む機能です。

本記事では、Background mode を単なる「非同期フラグ」として扱わず、運用まで含めた設計パターンとして整理します。公式ドキュメントにある要点に加え、実運用で起こりやすい詰まりどころ(重複実行、再試行地獄、Webhook処理遅延、キュー設計)を、個人開発で実装しやすい形に落とし込みます。冗談は軽め、運用は重めで行きます。

※ Responses API:OpenAIのエージェント向け中核APIです。ツール呼び出しや長時間処理に向いた拡張が進んでいます。
※ Background mode:レスポンス生成を非同期化し、ポーリングやWebhookで完了を受ける実行方式です。

Background modeの基本動作:同期思考をいったん捨てる

Background mode の本質はシンプルです。background: true を付けてリクエストすると、レスポンスは「その場で完成品を返す」より先に「ジョブの状態」を返します。代表的には queued(待機)→ in_progress(実行中)→ 終端状態、という流れです。終端に入るまで、responses.retrieve で状態を追跡します。

この仕組みが必要になるのは、モデルの推論時間が長くなったときです。OpenAI公式の案内でも、長時間タスクを同期接続で抱え続けるより、Background mode で安定化させる設計が推奨されています。特に複雑な推論・調査・大きめの出力で効果が出ます。

実装の最小形は次です。

from openai import OpenAI
from time import sleep

client = OpenAI()

resp = client.responses.create(
    model="gpt-5.4",
    input="競合調査レポートを作成してください。",
    background=True,
)

while resp.status in {"queued", "in_progress"}:
    sleep(2)
    resp = client.responses.retrieve(resp.id)

print(resp.status)
print(resp.output_text)

このコード自体は平和ですが、実運用では「ポーリングだけ」だと穴が出ます。たとえば、サーバープロセスが途中で落ちたら誰が続きを見るのか、同じジョブを誤って二重実行したらどちらを採用するのか、などです。ここから先が運用設計の本番です。

なお、公式ガイドには「Background modeはレスポンスデータを約10分保持してポーリングを支える」旨の記載があります。つまり、取りに行く責務を後回しにしすぎると、取りこぼしリスクが上がります。放置プレイは人間関係だけで十分です。

ポーリング設計の実務:2秒固定よりバックオフ

サンプルコードは2秒固定ポーリングが多いですが、本番ではバックオフを入れるのが安全です。理由は3つあります。

  1. API呼び出し回数の無駄を減らせる
  2. 一時的な障害時に自分で自分をDoSしにくい
  3. 長時間ジョブの全体コストを読みやすくできる

個人開発でおすすめなのは「段階的バックオフ+上限」です。たとえば 2秒 → 4秒 → 8秒 → 15秒(上限)まで伸ばし、終端に入るまで続けます。失敗状態になったら、即時再試行ではなく「入力を短くする」「ツール呼び出しを分割する」などの設計変更も検討します。再試行は万能薬ではなく、時々ただの連打です。

import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

async function waitResponse(id: string) {
  let delayMs = 2000;
  const maxDelayMs = 15000;

  while (true) {
    const r = await client.responses.retrieve(id);
    if (r.status !== "queued" && r.status !== "in_progress") return r;

    await new Promise((resolve) => setTimeout(resolve, delayMs));
    delayMs = Math.min(Math.floor(delayMs * 1.7), maxDelayMs);
  }
}

さらに、ジョブ管理テーブルを持つと運用が楽になります。最低限、job_id、response_id、status、attempt、updated_at を保存しておくと、障害調査が速くなります。ログを見ながら「このID、どこ行った?」と唱える回数が減ります。これは開発者のメンタルヘルスに効きます。

Webhook連携:完了通知は「速く受けて、重く処理しない」

Background mode をちゃんと運用するなら、Webhookはほぼ必須です。OpenAIのWebhooksガイドでも、response.completed などのイベントを受けて追跡する形が示されています。ここで重要なのは、Webhookエンドポイントを「重い処理の実行場所」にしないことです。

原則は次の通りです。

  • 署名検証(SDKの unwrap など)を通す
  • 受信イベントを永続化する
  • すぐ2xxを返す
  • 重い処理は別ワーカーで実行する

Webhookは通知チャネルであって、アプリ本体の処理系ではありません。エンドポイント内で外部API呼び出しを10本並べると、タイムアウト→再配信→重複処理のコンボが発生します。これは「イベント駆動」ではなく「イベント駆逐」です。

Pythonの最小例は次です(動作未検証、公式サンプル方針ベース)。

import os
from flask import Flask, request, Response
from openai import OpenAI, InvalidWebhookSignatureError

app = Flask(__name__)
client = OpenAI(webhook_secret=os.environ["OPENAI_WEBHOOK_SECRET"])

@app.route("/webhook/openai", methods=["POST"])
def openai_webhook():
    try:
        event = client.webhooks.unwrap(request.data, request.headers)
        # ここではDBに event.id / event.type / event.data.id を保存するだけ
        return Response(status=200)
    except InvalidWebhookSignatureError:
        return Response("invalid signature", status=400)

Webhookは再送される前提で作るべきです。したがって、event.id をキーに冪等化します。

※ 冪等(べきとう):同じ操作を複数回実行しても結果が変わらない性質です。再送・再実行の多い自動化では生命線です。

冪等性と重複排除:AIジョブ運用の「静かな主役」

AI運用の障害は派手な500エラーより、静かな重複で起こります。同じ指示が二重実行され、Slackに2回投稿され、DBに似たレコードが2つ入る。誰も気づかないまま後工程が壊れます。怖いのは、失敗より成功が2回起こることです。

対策は難しくありません。

  1. リクエスト投入時に client_job_id を発行
  2. client_job_id と response_id を紐付け保存
  3. 完了処理は client_job_id 単位で一度だけ実行
  4. Webhookイベントは event.id で重複排除

D1やPostgreSQLなら、ユニーク制約でほぼ片付きます。

CREATE TABLE IF NOT EXISTS ai_jobs (
  client_job_id TEXT PRIMARY KEY,
  response_id TEXT NOT NULL,
  status TEXT NOT NULL,
  final_output TEXT,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS webhook_events (
  event_id TEXT PRIMARY KEY,
  event_type TEXT NOT NULL,
  response_id TEXT,
  received_at TEXT NOT NULL
);

この2テーブルを置くだけで、障害時の復旧が段違いに楽になります。何が起きたか再構成できるからです。逆にここがないと、問題発生時に「たぶんこれが最新」と勘で進めることになります。勘は料理では頼れますが、運用では裏切ります。

キュー連携で壊れにくくする:Cloudflare Queuesの実践ポイント

Background mode とキューは相性が良いです。理由は責務分離が明確になるためです。

  • API受付:ジョブ投入だけ
  • キュー消費:Responses API実行
  • Webhook受信:完了イベント受領
  • 後処理ワーカー:保存・通知・再実行判定

Cloudflare Queuesのドキュメントでは、max_batch_size や max_batch_timeout の調整、個別 ack() / retry() の使い分けが説明されています。ここで大事なのは「一部成功を全失敗にしない」ことです。バッチ全体をまとめて失敗扱いにすると、既に成功した処理まで巻き戻されます。

外部書き込み(DB更新、通知送信)が絡む場合は、メッセージごとの ack() を徹底した方が事故が減ります。失敗した1件だけ retry() すればよいからです。バッチ再配信による重複副作用を小さくできます。

export default {
  async queue(batch, env) {
    for (const msg of batch.messages) {
      try {
        // 1件分の処理(冪等キー必須)
        // 成功したら確定
        msg.ack();
      } catch (e) {
        // 失敗したメッセージだけ再試行
        msg.retry();
      }
    }
  },
};

※ DLQ(Dead Letter Queue):再試行上限を超えても処理できないメッセージの退避先です。腐らせず隔離する保健室のような存在です。

実装テンプレート:個人開発で始める最小ランブック

ここまでを踏まえて、私が個人開発で採用しやすい最小運用フローを示します。

  1. 受付API
    リクエストを受けたら client_job_id を採番してキューへ投入。APIは即時200を返す。

  2. 実行ワーカー
    キュー消費で responses.create(background=true) を実行し、response_id をDB保存。

  3. 状態追跡
    Webhookを第一優先で受ける。補助として定期ポーラーを用意し、取りこぼしを回収。

  4. 完了処理
    response_id から最終出力を取得し、成果物保存・通知実行。処理は client_job_id 単位で冪等化。

  5. 失敗運用
    failed や長時間 queued が続いたジョブをアラート対象にし、入力分割かモデル変更で再実行。

  6. 可観測性
    queued秒数、in_progress秒数、成功率、再試行回数をメトリクス化。

この構成のメリットは、問題の位置が分かりやすいことです。どこで詰まったかが見えれば、直せます。逆に全部を1プロセスで抱えると、失敗時に「どこで死んだか不明」という最悪のデバッグ体験になります。

2026年の実務トレンドと、今やるべきこと

2026年の開発トレンドは「AIを使う」から「AIジョブを運用する」へ移っています。OpenAI側ではResponses APIにBackground modeやWebhook連携が整備され、Cloudflare側ではキュー運用の粒度(batch/ack/retry)の実践知が強化されています。さらにGitHub Copilotのデータ利用方針アップデートのように、便利さだけでなくガバナンスを含めて設計する流れが加速しています。

つまり、これからの個人開発は次の2軸で差が出ます。

  • 速く作る能力
  • 壊れても戻せる能力

前者はみんな強くなっています。後者で差が開きます。Background modeはその後者を底上げする強いカードです。非同期化は地味ですが、地味な設計ほど本番で効きます。派手なデモは拍手されますが、夜中に自分を助けるのは地味な運用です。未来の自分から感謝されるのは、たぶんこちらです。

最後に実務メモです。最初の一歩は「同期処理を全部置き換える」ではありません。まずは重い1ユースケースだけBackground modeに切り出し、Webhook+冪等キーを付ける。そこから観測を足していけば十分です。段階導入で勝てます。筋トレと同じで、初日から100kgは持たなくて大丈夫です。フォーム(設計)を整えた人が、最後に強いです。


参考リンク

  1. OpenAI API Docs: Background mode
    https://developers.openai.com/api/docs/guides/background
  2. OpenAI API Docs: Webhooks guide
    https://developers.openai.com/api/docs/guides/webhooks
  3. OpenAI API Reference: Webhook events
    https://developers.openai.com/api/docs/api-reference/webhook-events
  4. OpenAI: New tools and features in the Responses API
    https://openai.com/index/new-tools-and-features-in-the-responses-api/
  5. OpenAI Developers Blog: OpenAI for Developers in 2025
    https://developers.openai.com/blog/openai-for-developers-2025
  6. Cloudflare Docs: Queues batching, retries, delays
    https://developers.cloudflare.com/queues/configuration/batching-retries/
  7. Cloudflare Blog: Welcome to Agents Week
    https://blog.cloudflare.com/welcome-to-agents-week/
  8. GitHub Blog: Copilot interaction data usage policy update
    https://github.blog/news-insights/company-news/updates-to-github-copilot-interaction-data-usage-policy/
Tags #OpenAI #Responses API #Background mode #Webhook #個人開発 #自動化