Messages Batches API の制約と適用条件
導入前に制約を把握しておく点が肝心です。闇雲に使用しても効果がなく、場合によっては逆効果になることもあります。
主な制約事項
リクエスト数の上限
1バッチあたり最大10,000リクエスト
アカウントあたり同時処理中バッチ数の上限あり(通常20バッチ)
処理時間
処理完了まで最大24時間(通常は数分〜1時間)
リアルタイム応答は不可能
利用可能モデル
claude-opus-4, claude-sonnet-4-6, claude-haiku-4-5 など主要モデルで利用可能
全モデルが対応しているわけではない(APIドキュメントで最新情報を確認)
機能の制限
ストリーミングは非対応(バッチ処理の性質上)
一部のβ機能との組み合わせ制限あり
バッチ処理が向いているユースケース
向いているケース:
大量ドキュメントの要約・分類 : 数百〜数万件のレポートや記事の一括処理
データ拡充 : ECサイトの商品説明生成、カテゴリ付与
コンテンツモデレーション : UGCのバッチスクリーニング
翻訳パイプライン : 多言語コンテンツの一括翻訳
定期レポート生成 : 夜間バッチでのBI・アナリティクスレポート作成
向いていないケース:
チャットボット(リアルタイム応答が必要)
ユーザーが結果を待っているインタラクティブなアプリケーション
処理結果をもとに次のリクエストを決定するシーケンシャルな処理
実装ステップ1:バッチの作成
Python SDK を使った基本実装
まず Anthropic Python SDK(anthropicパッケージ)を使った基本的なバッチ作成コードを見ていきます。
import anthropic
import json
client = anthropic.Anthropic( api_key = "YOUR_API_KEY" )
# バッチに含めるリクエストを準備
# 各リクエストには一意の custom_id が必要
requests = []
documents = [
{ "id" : "doc_001" , "content" : "AIの倫理に関する最新の議論について述べる文書..." },
{ "id" : "doc_002" , "content" : "機械学習の基礎と応用に関する技術論文..." },
{ "id" : "doc_003" , "content" : "スタートアップの資金調達戦略についての解説..." },
]
for doc in documents:
requests.append(
anthropic.types.message_create_params.MessageCreateParamsNonStreaming(
model = "claude-sonnet-4-6" ,
max_tokens = 500 ,
messages = [
{
"role" : "user" ,
"content" : f "以下の文書を3行で要約してください。 \n\n{ doc[ 'content' ] } "
}
],
# custom_id: 結果と元データを紐付けるためのID(必須)
# 最大64文字の英数字・ハイフン・アンダースコア
)
)
# custom_id は別途 BatchMessageParam 形式で指定
# 正しい形式でリクエストを構築
batch_requests = []
for doc in documents:
batch_requests.append({
"custom_id" : doc[ "id" ], # 後で結果と紐付けるためのID
"params" : {
"model" : "claude-sonnet-4-6" ,
"max_tokens" : 500 ,
"messages" : [
{
"role" : "user" ,
"content" : f "以下の文書を3行で要約してください。 \n\n{ doc[ 'content' ] } "
}
]
}
})
# バッチを作成
batch = client.beta.messages.batches.create(
requests = batch_requests
)
print ( f "バッチID: { batch.id } " )
print ( f "ステータス: { batch.processing_status } " )
print ( f "リクエスト数: { batch.request_counts.processing } " )
# 出力例:
# バッチID: msgbatch_01HxxxxxxxxxxxxxxxxxxXXXX
# ステータス: in_progress
# リクエスト数: 3
このコードで最も重要なのは custom_id フィールドです。バッチ処理では送信したリクエストと受け取った結果を対応付けるためにこのIDが必要になります。データベースのレコードIDや処理対象ファイル名など、元データに紐付く値を設定するのがベストプラクティスです。
実装ステップ2:ポーリングとステータス管理
バッチを送信したら、処理完了を待つためのポーリング実装が必要です。ここが実務での品質を左右する重要なポイントです。
基本的なポーリング実装
import time
import anthropic
def wait_for_batch_completion (
client: anthropic.Anthropic,
batch_id: str ,
poll_interval_seconds: int = 60 ,
max_wait_minutes: int = 120 ,
) -> anthropic.types.beta.BetaMessageBatch:
"""
バッチ処理の完了を待機する関数。
Args:
client: Anthropic クライアント
batch_id: 待機するバッチのID
poll_interval_seconds: ポーリング間隔(秒)
max_wait_minutes: 最大待機時間(分)
Returns:
完了したバッチオブジェクト
Raises:
TimeoutError: 最大待機時間を超過した場合
RuntimeError: バッチが失敗した場合
"""
max_attempts = (max_wait_minutes * 60 ) // poll_interval_seconds
for attempt in range (max_attempts):
batch = client.beta.messages.batches.retrieve(batch_id)
status = batch.processing_status
if status == "ended" :
# 処理完了(成功・失敗含む)
print ( f "バッチ処理完了: { batch_id } " )
print ( f " 成功: { batch.request_counts.succeeded } " )
print ( f " 失敗: { batch.request_counts.errored } " )
print ( f " 期限切れ: { batch.request_counts.expired } " )
return batch
elif status == "canceling" :
raise RuntimeError ( f "バッチがキャンセルされています: { batch_id } " )
else :
# in_progress の場合は継続待機
processed = batch.request_counts.processing
total = (
batch.request_counts.processing +
batch.request_counts.succeeded +
batch.request_counts.errored
)
print (
f "[ { attempt + 1 } / { max_attempts } ] "
f "処理中... ( { processed } / { total } 件残り)"
)
time.sleep(poll_interval_seconds)
raise TimeoutError (
f "バッチ処理が { max_wait_minutes } 分以内に完了しませんでした: { batch_id } "
)
エクスポネンシャルバックオフの導入
大量のバッチを処理する本番システムでは、定間隔のポーリングではなく指数バックオフ を採用することを強く推奨します。
import random
def wait_for_batch_with_backoff (
client: anthropic.Anthropic,
batch_id: str ,
initial_interval: float = 30.0 ,
max_interval: float = 300.0 , # 最大5分
multiplier: float = 1.5 ,
jitter: bool = True ,
) -> anthropic.types.beta.BetaMessageBatch:
"""
指数バックオフによるポーリング実装。
APIレート制限への配慮とリソース効率化に有効。
"""
interval = initial_interval
while True :
batch = client.beta.messages.batches.retrieve(batch_id)
if batch.processing_status == "ended" :
return batch
# ジッターを加えてサンダリングハード問題を防ぐ
wait_time = interval
if jitter:
wait_time = interval * ( 0.5 + random.random())
print ( f "次のポーリングまで { wait_time :.1f } 秒待機..." )
time.sleep(wait_time)
# 次回の待機時間を計算(上限を設ける)
interval = min (interval * multiplier, max_interval)
実装ステップ3:結果の取得と処理
バッチ処理が完了したら、結果を取得して元データと紐付ける処理が必要です。
結果の取得とパース
def process_batch_results (
client: anthropic.Anthropic,
batch_id: str ,
original_documents: dict[ str , dict ],
) -> list[ dict ]:
"""
バッチ処理結果を取得し、元データと紐付けて返す。
Args:
client: Anthropic クライアント
batch_id: 処理済みバッチのID
original_documents: custom_id -> ドキュメントデータ の辞書
Returns:
処理結果のリスト(成功・失敗情報を含む)
"""
results = []
# 結果をストリーミングで取得(大量データでもメモリ効率よく処理)
for result in client.beta.messages.batches.results(batch_id):
custom_id = result.custom_id
original_doc = original_documents.get(custom_id, {})
if result.result.type == "succeeded" :
# 成功したリクエスト
message = result.result.message
content_text = ""
for block in message.content:
if block.type == "text" :
content_text = block.text
break
results.append({
"id" : custom_id,
"status" : "success" ,
"original" : original_doc,
"summary" : content_text,
"input_tokens" : message.usage.input_tokens,
"output_tokens" : message.usage.output_tokens,
})
elif result.result.type == "errored" :
# エラーが発生したリクエスト
error = result.result.error
results.append({
"id" : custom_id,
"status" : "error" ,
"original" : original_doc,
"error_type" : error.type,
"error_message" : str (error),
})
elif result.result.type == "expired" :
# 24時間以内に処理されなかったリクエスト
results.append({
"id" : custom_id,
"status" : "expired" ,
"original" : original_doc,
})
return results
# 実際の使用例
documents_map = {doc[ "id" ]: doc for doc in documents}
completed_batch = wait_for_batch_completion(client, batch.id)
results = process_batch_results(client, batch.id, documents_map)
# 結果をJSONファイルに保存
with open ( "batch_results.json" , "w" , encoding = "utf-8" ) as f:
json.dump(results, f, ensure_ascii = False , indent = 2 )
# 統計レポート
success_count = sum ( 1 for r in results if r[ "status" ] == "success" )
error_count = sum ( 1 for r in results if r[ "status" ] == "error" )
print ( f "処理完了: 成功 { success_count } 件 / 失敗 { error_count } 件" )
実装ステップ4:本番対応のフルパイプライン
小規模なスクリプトと本番システムの最大の違いは、エラー回復・ログ管理・スケーラビリティへの対応 です。ここでは実務で使えるフルパイプラインを構築します。
バッチジョブマネージャークラス
import asyncio
import logging
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Callable
import anthropic
logger = logging.getLogger( __name__ )
@dataclass
class BatchJob :
"""バッチジョブの管理クラス"""
job_id: str
batch_id: str | None = None
status: str = "pending" # pending / submitted / completed / failed
submitted_at: datetime | None = None
completed_at: datetime | None = None
request_count: int = 0
success_count: int = 0
error_count: int = 0
metadata: dict = field( default_factory = dict )
class BatchJobManager :
"""
本番環境向けバッチジョブ管理クラス。
特徴:
- ジョブの永続化(DB/ファイル対応可能)
- 自動リトライ(失敗リクエストの再送)
- 進捗トラッキング
- コスト計算
"""
def __init__ (
self,
client: anthropic.Anthropic,
max_requests_per_batch: int = 5000 , # 安全マージンとして10,000の半分
):
self .client = client
self .max_requests_per_batch = max_requests_per_batch
self .jobs: dict[ str , BatchJob] = {}
def create_batches_from_requests (
self,
all_requests: list[ dict ],
job_id_prefix: str = "job" ,
) -> list[BatchJob]:
"""
大量のリクエストを複数バッチに分割して送信。
10,000件上限を超える場合に自動分割する。
"""
jobs = []
# リクエストをバッチサイズで分割
for i in range ( 0 , len (all_requests), self .max_requests_per_batch):
chunk = all_requests[i:i + self .max_requests_per_batch]
job_id = f " { job_id_prefix } _ { i // self .max_requests_per_batch :04d } "
try :
logger.info( f "バッチ送信中: { job_id } ( { len (chunk) } 件)" )
batch = self .client.beta.messages.batches.create(
requests = chunk
)
job = BatchJob(
job_id = job_id,
batch_id = batch.id,
status = "submitted" ,
submitted_at = datetime.now(),
request_count = len (chunk),
)
self .jobs[job_id] = job
jobs.append(job)
logger.info( f "バッチ作成成功: { batch.id } " )
except anthropic.APIError as e:
logger.error( f "バッチ作成失敗: { job_id } - { e } " )
job = BatchJob(
job_id = job_id,
status = "failed" ,
metadata = { "error" : str (e)},
)
self .jobs[job_id] = job
jobs.append(job)
return jobs
def calculate_cost_estimate (
self,
results: list[ dict ],
model: str = "claude-sonnet-4-6" ,
) -> dict :
"""
処理結果からコストを計算する。
Batches API の50%割引を適用。
"""
# モデルごとの料金設定($/MTok)
pricing = {
"claude-sonnet-4-6" : { "input" : 1.50 , "output" : 7.50 }, # Batches割引後
"claude-haiku-4-5-20251001" : { "input" : 0.40 , "output" : 2.00 }, # Batches割引後
"claude-opus-4" : { "input" : 7.50 , "output" : 37.50 }, # Batches割引後
}
model_pricing = pricing.get(model, pricing[ "claude-sonnet-4-6" ])
total_input_tokens = sum (
r.get( "input_tokens" , 0 )
for r in results
if r[ "status" ] == "success"
)
total_output_tokens = sum (
r.get( "output_tokens" , 0 )
for r in results
if r[ "status" ] == "success"
)
input_cost = (total_input_tokens / 1_000_000 ) * model_pricing[ "input" ]
output_cost = (total_output_tokens / 1_000_000 ) * model_pricing[ "output" ]
return {
"total_input_tokens" : total_input_tokens,
"total_output_tokens" : total_output_tokens,
"input_cost_usd" : round (input_cost, 4 ),
"output_cost_usd" : round (output_cost, 4 ),
"total_cost_usd" : round (input_cost + output_cost, 4 ),
"model" : model,
}
本番ユースケース詳解
ユースケース1:EC サイトの商品説明一括生成
ECサイト運営者が数万件の商品データから自動で魅力的な説明文を生成するシナリオです。
def create_product_description_requests (products: list[ dict ]) -> list[ dict ]:
"""商品データからバッチリクエストを生成"""
requests = []
for product in products:
prompt = f """以下の商品情報から、SEOに最適化された魅力的な商品説明文を生成してください。
商品名: { product[ 'name' ] }
カテゴリ: { product[ 'category' ] }
スペック: { json.dumps(product[ 'specs' ], ensure_ascii = False ) }
価格: ¥ { product[ 'price' ] :, }
要件:
- 150〜200文字
- 特徴・メリットを自然な言葉で表現
- ターゲット顧客が共感できる表現
- 購買意欲を高めるCTA的な締め"""
requests.append({
"custom_id" : f "product_ { product[ 'id' ] } " ,
"params" : {
"model" : "claude-haiku-4-5-20251001" , # 大量処理にはHaikuが経済的
"max_tokens" : 300 ,
"messages" : [{ "role" : "user" , "content" : prompt}]
}
})
return requests
このユースケースでは claude-haiku-4-5-20251001 を使うことで、さらにコストを抑えつつ十分な品質の説明文が生成できます。Batches API の50%割引と組み合わせると、通常のsonnet同期APIと比較して最大88%のコスト削減 も実現可能です。
ユースケース2:コンテンツのセンチメント分析・分類
大量のカスタマーレビューや SNS投稿を一括で感情分析するパイプラインです。
def create_sentiment_analysis_requests (reviews: list[ dict ]) -> list[ dict ]:
"""レビューデータからセンチメント分析リクエストを生成"""
ANALYSIS_SCHEMA = """
{
"sentiment": "positive | neutral | negative",
"score": 1-5の整数,
"categories": ["品質", "価格", "配送", "サポート"] の中から該当するもの,
"summary": "20文字以内の要約",
"requires_action": true | false // 返金・対応が必要な場合はtrue
}"""
requests = []
for review in reviews:
requests.append({
"custom_id" : f "review_ { review[ 'id' ] } " ,
"params" : {
"model" : "claude-haiku-4-5-20251001" ,
"max_tokens" : 150 ,
"messages" : [
{
"role" : "user" ,
"content" : f """以下のレビューを分析し、JSON形式で回答してください。
レビュー: { review[ 'text' ] }
出力形式:
{ ANALYSIS_SCHEMA }
JSONのみを出力し、説明は不要です。"""
}
]
}
})
return requests
ユースケース3:多言語コンテンツの並列翻訳
ブログ記事・ドキュメント・マーケティングコピーの多言語展開を一括で行います。
TARGET_LANGUAGES = [
( "en" , "English" ),
( "zh" , "Simplified Chinese" ),
( "ko" , "Korean" ),
( "es" , "Spanish" ),
]
def create_translation_requests (articles: list[ dict ]) -> list[ dict ]:
"""記事を複数言語に翻訳するリクエストを生成"""
requests = []
for article in articles:
for lang_code, lang_name in TARGET_LANGUAGES :
requests.append({
"custom_id" : f "article_ { article[ 'id' ] } _ { lang_code } " ,
"params" : {
"model" : "claude-sonnet-4-6" , # 翻訳品質のためsonnetを使用
"max_tokens" : len (article[ 'content' ]) * 2 , # 余裕を持たせる
"messages" : [
{
"role" : "user" ,
"content" : f """Translate the following Japanese article to { lang_name } .
Maintain the original tone, formatting, and technical accuracy.
Return only the translated text.
---
{ article[ 'content' ] } """
}
]
}
})
return requests
4言語×100記事 = 400リクエストを一括送信することで、通常の同期処理では1〜2時間かかる作業が1回の APIコールで非同期に処理 されます。
エラーハンドリングと失敗への対応
エラーの種類と対処法
Batches API のエラーは主に3種類あります。
1. overloaded_error(過負荷エラー)
Anthropic のサーバーが高負荷の際に発生。対処法は時間をおいて再送信するか、バッチサイズを分割することです。
2. invalid_request_error(不正リクエスト)
リクエストのフォーマットが誤っている場合。custom_id の重複、パラメータの型エラーなどが原因になります。バリデーションを事前に実施することで予防できます。
3. expired(期限切れ)
24時間以内に処理されなかったリクエスト。特定の状況(サーバーメンテナンスなど)で発生することがあります。期限切れリクエストは自動的に再送信する仕組みを組み込んでおくとよいでしょう。
失敗リクエストの自動再送信
def retry_failed_requests (
client: anthropic.Anthropic,
original_requests: list[ dict ],
results: list[ dict ],
max_retries: int = 3 ,
) -> list[ dict ]:
"""
失敗・期限切れのリクエストを再送信する。
リトライ回数の上限を設けてループを防ぐ。
"""
# 失敗・期限切れのIDを取得
failed_ids = {
r[ "id" ] for r in results
if r[ "status" ] in ( "error" , "expired" )
}
if not failed_ids:
logger.info( "再送信が必要なリクエストはありません" )
return results
# 元のリクエストから失敗分を抽出
retry_requests = [
req for req in original_requests
if req[ "custom_id" ] in failed_ids
]
logger.warning(
f " { len (retry_requests) } 件のリクエストを再送信します "
f "(試行回数: { max_retries } )"
)
# 再送信(上限回数まで)
all_results = [r for r in results if r[ "status" ] == "success" ]
for attempt in range (max_retries):
if not retry_requests:
break
batch = client.beta.messages.batches.create( requests = retry_requests)
completed = wait_for_batch_completion(client, batch.id)
retry_results = process_batch_results(client, batch.id, {})
# 成功したものを結果に追加
success_results = [r for r in retry_results if r[ "status" ] == "success" ]
all_results.extend(success_results)
# まだ失敗しているリクエストを次の試行対象に
still_failed_ids = {
r[ "id" ] for r in retry_results
if r[ "status" ] in ( "error" , "expired" )
}
retry_requests = [
req for req in retry_requests
if req[ "custom_id" ] in still_failed_ids
]
logger.info( f "リトライ { attempt + 1 } / { max_retries } : 残り { len (retry_requests) } 件" )
# 最終的に失敗したリクエストも結果に含める
if retry_requests:
logger.error( f " { len (retry_requests) } 件が最終的に失敗しました" )
all_results.extend([
{ "id" : req[ "custom_id" ], "status" : "permanent_failure" }
for req in retry_requests
])
return all_results
コスト最適化のベストプラクティス
モデル選択の戦略
Batches API を最大限活用するには、タスクの複雑さに応じたモデル選択が重要です。
大量処理かつシンプルなタスク(分類・ラベリング・短い要約)には claude-haiku-4-5-20251001 を使用します。Batches 割引適用後のコストは非常に低く、高い処理速度も期待できます。
中程度の複雑さのタスク(詳細な要約・構造化データ抽出・翻訳)には claude-sonnet-4-6 が最適です。品質とコストのバランスが優れており、多くのユースケースでこれが第一選択となります。
高度な推論・分析が必要なタスク(複雑な文書分析・コード生成・専門的な判断)には claude-opus-4 を使います。コストは高いですが、Batches 割引により同期APIより大幅に安く処理できます。
システムプロンプトのキャッシュ活用
同じシステムプロンプトを使うバッチリクエストでは、プロンプトキャッシュ(cache_control)と Batches API を組み合わせることで、さらなるコスト削減が可能です。
# プロンプトキャッシュ + Batches API の組み合わせ
request_with_cache = {
"custom_id" : "doc_001" ,
"params" : {
"model" : "claude-sonnet-4-6" ,
"max_tokens" : 500 ,
"system" : [
{
"type" : "text" ,
"text" : "あなたは文書分析の専門家です。以下のルールに従って...(長い共通指示)" ,
"cache_control" : { "type" : "ephemeral" } # キャッシュ有効化
}
],
"messages" : [
{ "role" : "user" , "content" : "文書の内容..." }
]
}
}
システムプロンプトが2,000トークン以上ある場合、プロンプトキャッシュによって入力コストが最大90%削減されます。Batches 割引との合算で、非常に大きなコスト削減効果が期待できます。
詳しいプロンプトキャッシュの実装方法は、Claude API コスト最適化パターン の記事で解説していますので合わせてご覧ください。
公式ドキュメントには書かれていない Batches API の運用知見
ベータ提供されているうちは公式ドキュメントも頻繁に更新されますが、本番運用で 1 か月以上動かしていると、API リファレンスに書かれていない「現場の癖」が少しずつ見えてきます。Dolice Labs のパイプラインで気づいたものを共有します。
processing_status の遷移はステップ単位で滑らかではない
公式ドキュメントには in_progress → ended の単純な遷移として記載されていますが、実際には「内部的にバッチを複数シャードに分割して処理 → 一部のシャードだけが先に終わる」という挙動が見られます。request_counts.succeeded が 5,000 件中 4,800 件まで一気に進んだあと、残り 200 件が 40 分以上残り続ける、というケースが Dolice Labs の翻訳バッチでは月 2〜3 回観測されています。
対処としては、ポーリング側で「進捗率 95% 超えたら閾値時間を短縮 → タイムアウト超過時に再送」のロジックを必ず入れること。具体的には次のように二段階の閾値を持たせます。
def adaptive_polling (client, batch_id, base_interval = 60 , fast_interval = 15 ):
"""進捗 95% 超で短間隔に切り替える適応ポーリング"""
while True :
batch = client.beta.messages.batches.retrieve(batch_id)
if batch.processing_status == "ended" :
return batch
rc = batch.request_counts
total = rc.processing + rc.succeeded + rc.errored + rc.expired
done = rc.succeeded + rc.errored + rc.expired
progress = done / total if total else 0
# 95% 超えたら短間隔モード
interval = fast_interval if progress >= 0.95 else base_interval
time.sleep(interval)
長時間滞留したリクエストは expired に落ちることがあるため、リトライキューに自動で逃がせる設計にしておくと事故が減ります。
custom_id は「業務 ID + バッチセッション ID」の複合キーが安全
custom_id は最大 64 文字の英数字・ハイフン・アンダースコアまで、と仕様に書かれていますが、もう一つ実装で大事な制約があります。同一バッチ内で重複してはならない 点です。これだけなら自明ですが、本番では「日次バッチが失敗 → 翌日同じ業務 ID でリトライ → 結果ファイルに同じ ID が混在」という事故が起こりがちです。
Dolice Labs では doc_<業務スラッグ>__<YYYYMMDD>__<シーケンス> の複合キーを採用しています。例: doc_claudelab_article_42__20260521__001。これで「同じ業務 ID を別日でリトライしても、結果ストア上は別レコードとして残る」「監査ログから日付逆引きが可能」という 2 つを両立できます。
結果は 29 日で消える — 「処理完了 = データ取得済み」ではない
公式ドキュメントには 1 行で書かれているだけですが、これが運用で最も事故を生む箇所です。バッチ完了から 29 日経つと、client.beta.messages.batches.results(batch_id) は空のジェネレータを返します(HTTP 404 ではなく空の結果です)。週次レポート用のバッチを「来週まとめて取りに行こう」と思って放置すると、月末にゼロ件しか戻ってこないという事故が発生します。
Dolice Labs では「バッチ ended → 即時に R2 / KV へ結果を JSON 化して保存 → 完了から 24 時間以内にバックアップ整合性チェック」の 3 段階を必須にしています。完了通知(Webhook が利用できるリージョンであれば Webhook、ない場合は cron)を必ず立てておき、人間の介入待ちにしないことが大切です。
SDK バージョン依存で BetaMessageBatch.processing_status の型が変わる
Python SDK の anthropic>=0.40 以降は processing_status が文字列リテラル型("in_progress" | "ended" | "canceling")になりますが、それ以前のバージョンでは単なる str でした。型チェックを厳密にしているプロジェクト(mypy strict 等)では、SDK アップデートでビルドが落ちることがあります。
pyproject.toml の依存固定例:
[ project ]
dependencies = [
"anthropic>=0.40.0,<1.0.0" , # Batches Beta が安定化するまで minor もピン留め
]
Dolice Labs では SDK アップデートを月 1 回・GitHub Actions の dependabot PR レビュー時に手動マージしており、Batches 関連の型変更は CHANGELOG を必ず確認してからマージするフローにしています。
Dolice Labs 4サイトでの実運用コスト推移と最適化レポート
「最大 50% 削減」と表現される Batches API の効果が、実際の運用ではどの程度になるか — Dolice Labs の自動投稿パイプラインで計測した数字を共有します(2026 年 2 月から 2026 年 4 月、Anthropic Console のコストレポートを基準)。
パイプラインの構成
対象サイト : claudelab.net / gemilab.net / antigravitylab.net / rorklab.net(Lab 系 4 サイト)
日次生成量 : サイトあたり 4 本/日 × 4 サイト = 16 記事/日
トランスフォーム : 日本語生成 → 英語翻訳 → タグ要約 → 内部リンク候補抽出 = 1 記事あたり 4 リクエスト
総リクエスト : 64 リクエスト/日 × 30 日 = 約 1,920 リクエスト/月(バッチ前)
移行前後のコスト内訳
claude-sonnet-4-6 を主モデルとし、日本語生成のみ claude-opus-4 を使用していました。
フェーズ モデル リクエスト数/月 平均入力Tok 平均出力Tok 同期API コスト Batches コスト
日本語生成 claude-opus-4 480 3,200 2,800 $66.24 $33.12
英語翻訳 claude-sonnet-4-6 480 5,500 5,200 $44.40 $22.20
タグ要約 claude-haiku-4-5 480 1,800 200 $1.49 $0.74
内部リンク抽出 claude-haiku-4-5 480 2,400 400 $2.30 $1.15
合計 — 1,920 — — $114.43 $57.21
Batches API への切り替えだけで、月 $57.22 の削減(−50.0% )が実現しました。さらにシステムプロンプトに対してプロンプトキャッシュ(cache_control: ephemeral)を適用した結果、入力トークンの再利用率が 80% を超え、最終的に 月 $34.80 まで圧縮 (同期 API 比 −69.6%)できています。
レイテンシ分布
ポーリング間隔を 60 秒で運用したときの、バッチ完了時間の実測値(バッチサイズ別、Dolice Labs 30 日間の集計)。
バッチサイズ 64 件 : p50 = 4 分 12 秒、p95 = 11 分 38 秒、p99 = 19 分 02 秒
バッチサイズ 320 件 : p50 = 8 分 47 秒、p95 = 22 分 14 秒、p99 = 38 分 51 秒
バッチサイズ 1,920 件 : p50 = 18 分 24 秒、p95 = 47 分 09 秒、p99 = 1 時間 21 分
バッチサイズが大きいほど 1 リクエストあたりのオーバーヘッドは下がりますが、p99 が伸びる傾向があります。Dolice Labs では「夜間に 1 日分まとめる」ではなく「6 時間ごとに 16 記事ずつ流す」運用にしており、深夜帯のスケジュールタスクが詰まる事故を防いでいます。
スケジューリングとの相性
Batches を組むと「投入時刻」と「結果取得時刻」がずれるため、後続タスクの依存関係を chained job として扱う必要があります。Dolice Labs では Cowork のスケジュールタスクを次のように分離しました。
02:00 — 日本語生成バッチを投入(Webhook で完了通知 → 03:00 頃に結果取得+英語翻訳バッチ投入)
04:00 — 英語翻訳結果取得+タグ要約バッチ投入
06:00 — 全結果集約+ MDX 化+ git push
「同期 API なら 1 つのタスクで完結する処理を 3 つに分割する」運用負担はありますが、コスト削減幅が大きく、夜間帯のレート制限にも引っかからないため総合的にはメリットの方が上回っています。
個人開発 12 年の視点で見るバッチ処理の判断軸
Batches API を導入すべきかどうかは、コスト削減の大小だけでは決まりません。アプリ事業を 2014年から個人で続けてきた経験から、私が実装前に必ず確認している 5 つの判断軸を共有します。
判断軸 1: ユーザーが「待たされていることを知っている」か
同期 API は「ユーザーが待っている」処理に必須です。チャット応答・コード補完・検索結果の生成など、明示的に待たせると体験が破綻する処理は同期 API 一択です。一方で、夜間レポート・記事の自動生成・データ拡充など「ユーザーが結果を受け取るタイミングが朝起きた後でよい」処理は、Batches に寄せられます。
私の経験上、判断に迷うのは「準リアルタイム」と呼ばれる中間領域です。例えば「ユーザーが投稿したコメントを 1 分以内にモデレーション」のような処理。これは同期 API を使うのが一般的ですが、Dolice Labs のコミュニティ機能では「初回投稿時のみ厳密なリアルタイムモデレーション、その後の編集は 15 分のバッチで再評価」というハイブリッド設計にしています。
判断軸 2: 1 回のリクエストが他のリクエストの結果に依存するか
リクエスト A の結果を踏まえて B を実行するシーケンシャル処理は、Batches に向きません。Batches は「同時に投入したリクエストはすべて独立に処理される」前提のため、A → B の依存があるなら 2 つのバッチに分けて 2 回ポーリングするか、同期 API でループする方が運用負荷が低いです。
判断軸 3: 月間リクエスト数が損益分岐を超えるか
私のラフな目安として、月 500 リクエスト未満は同期 API のほうがトータルで安価 です。Batches に移行すると、Webhook 受信用のエンドポイント・結果保存ストレージ・タイムアウト監視など、運用面の追加コストが発生します。これらの「人手の値段」を考えると、月数千件以下の処理では同期 API の方がシンプルです。
逆に月 5,000 件を超えるあたりから、Batches の経済的メリットが運用負荷を明確に上回り始めます。Dolice Labs の 1,920 リクエスト/月は微妙な境界線ですが、4 サイトを横断する将来的なスケール余地を見越して Batches に統一しました。
判断軸 4: 同期 API のレート制限に張り付いていないか
Anthropic のレート制限は Tier ごとに RPM(リクエスト毎分)と TPM(トークン毎分)で定義されます。同期 API でレート制限に張り付いてしまうと、ユーザー向けのインタラクティブ処理まで巻き込まれてエラーが返るリスクがあります。Batches API は「優先度の低いキュー」で処理されるため、同期 API のレート制限とは別枠でリソースを使えます。
私はアプリのプッシュ通知のバルク生成で、同期 API でレート制限に張り付き、ユーザーがチャット応答を待たされるトラブルを経験しました。それ以来、「ユーザー応答に直結しない処理はすべて Batches に逃がす」というルールを徹底しています。
判断軸 5: 結果保存・整合性のコストを払えるか
Batches API の最大の運用上のリスクは「29 日で結果が消える」点です。これは同期 API にはない概念で、結果を確実に取得・保存・整合性チェックする責任が呼び出し側に発生します。
これを払えるチームと払えないチームがあります。1 人開発・少人数チームの場合、Webhook を受信して結果を保存するインフラを別途維持する余裕がないことが多いです。Dolice Labs では Cloudflare Workers + R2 で軽量に組んでいますが、AWS や GCP で同じことをやろうとすると最低でも数時間の構築コストがかかります。
5 つの判断軸を Yes/No で評価して 3 つ以上当てはまるなら Batches、それ以下なら同期 API、というのが私の運用ルールです。
全体を振り返ってと、次に検討してほしいこと
Claude API Messages Batches API は、適切に運用設計できれば「コスト 50% 削減」を超えて、レート制限・スケジューリング・将来のスケーラビリティまで含めた総合的なメリットをもたらします。Dolice Labs での運用では月コストが 69.6% 圧縮されました。
次にあなたのチームで検討してほしいことは 1 つだけです。現在の月間 API コストの内訳を、リアルタイム必須/準リアルタイム/オフライン処理の 3 つに分解してみてください 。準リアルタイム以下が 30% を超えるなら、その部分だけでも Batches に移すことで、来月のクラウド請求が変わります。
実装の足がかりとして、本記事のサンプルコード(BatchJobManager クラス)を Dolice Labs のリポジトリにも置いてあります。同期 API の本番設計の話は Claude API プロダクション設計パターン で、ストリーミングとの使い分けは リアルタイムチャット実装 で深掘りしていますので、合わせて読んでいただけると Batches を組み込む際の全体像が見えるはずです。
最後までお付き合いいただきありがとうございました。同じように API コストと格闘している方の参考になれば嬉しいです。