CLAUDE LABEN
MODEL — Claude Sonnet 5が全プランの既定モデルになり、計画・ツール利用・自律実行が強化されましたPRICE — Sonnet 5は導入価格が100万トークンあたり入力$2・出力$10で、8月31日まで適用されますMODEL — Sonnet 5の性能はOpus 4.8に迫りつつ、より低価格でエージェントを常時運用できますCODE — Claude CodeがSonnet 5を既定モデルに採用し、ネイティブ1Mトークンのコンテキストに対応しましたCODE — Claude Codeにサンドボックスの資格情報ブロックと組織単位のモデル制限が追加されましたCLOUD — ClaudeがMicrosoft Foundry(Azure)で一般提供され、Azureネイティブで利用できますMODEL — Claude Sonnet 5が全プランの既定モデルになり、計画・ツール利用・自律実行が強化されましたPRICE — Sonnet 5は導入価格が100万トークンあたり入力$2・出力$10で、8月31日まで適用されますMODEL — Sonnet 5の性能はOpus 4.8に迫りつつ、より低価格でエージェントを常時運用できますCODE — Claude CodeがSonnet 5を既定モデルに採用し、ネイティブ1Mトークンのコンテキストに対応しましたCODE — Claude Codeにサンドボックスの資格情報ブロックと組織単位のモデル制限が追加されましたCLOUD — ClaudeがMicrosoft Foundry(Azure)で一般提供され、Azureネイティブで利用できます
記事一覧/API & SDK
API & SDK/2026-07-02上級

Message Batches に2万件投げたら41件だけ静かに欠けていたとき — 部分失敗を照合して再投入する運用メモ

Message Batches API の processing_status: ended は全件成功を意味しません。errored・expired が結果に静かに混ざる仕組みと、custom_id 台帳で欠落を照合し二重処理なく再投入する実装を、実運用の計測値とともに整理します。

Claude API100Message Batchesバッチ処理4エラーハンドリング6運用設計9

プレミアム記事

バッチの状態が ended になったのを確認して、その日は安心して眠りました。異変に気づいたのは3日後です。

タグ再分類のために Message Batches API へ投入した 20,000 件のうち、集計テーブルに入っていたのは 19,959 件。41 件が、エラーログも例外もないまま欠けていました。個人開発で複数の技術ブログを並行運用している関係で、記事メタデータの一括処理にはバッチをよく使うのですが、この「静かな欠落」は私自身、初めての経験でした。

原因を追うと、欠落は API の不具合ではなく、私の思い込みでした。processing_status: "ended" は「バッチという箱の処理が終わった」ことしか意味せず、中身の 1 件 1 件が成功したかどうかは別の話だったのです。

この記録では、結果ストリームのどこで件数が欠けるのか、custom_id 台帳による照合で欠落をゼロにする実装、そして二重処理を起こさない再投入の設計を、実運用の計測値と一緒に残します。

「ended」は全件成功ではない — 4つの終わり方

Message Batches の結果は、リクエスト 1 件ごとに result.type を持ちます。終わり方は 4 つです。

result.type意味起きやすい状況取るべき対応
succeeded正常完了。message が入っているそのまま集計
errored個別リクエストの失敗。error オブジェクトが入るinvalid_request(パラメータ不正)、api_error、overloadedエラー種別で分岐(後述)
canceledバッチのキャンセルに巻き込まれた手動キャンセル時の未処理分再投入
expired24時間の処理ウィンドウ内に完了しなかった大規模バッチ+混雑時間帯再投入

重要なのは、errored や expired が混ざっていてもバッチ全体は正常に ended になることです。例外は投げられませんし、SDK が警告してくれるわけでもありません。成功分だけを素直にループで拾うコードを書くと、失敗分は誰にも知られずに消えます。

私の 41 件の内訳は、errored が 28 件(overloaded 系が 19、invalid_request が 9)、expired が 13 件でした。invalid_request の 9 件は、元データに混ざっていた空文字列の本文をそのまま投げていたもの。つまりリトライしても直らない失敗が混ざっていました。ここを区別せずに全件再投入すると、同じ 9 件が永遠に失敗し続けます。

台帳を先に作る — custom_id manifest の設計

照合の前提は「何を投げたか」を API の外に持っておくことです。投げた側の記録がなければ、結果と突き合わせる基準がありません。

送信時に、custom_id と入力内容のハッシュを JSONL の台帳(manifest)へ書き出します。

import anthropic
import hashlib
import json
from pathlib import Path
 
client = anthropic.Anthropic()  # ANTHROPIC_API_KEY は環境変数から
 
MANIFEST = Path("batch_manifest.jsonl")
 
def build_requests(items: list[dict]) -> list[dict]:
    """items: [{"id": "article-0001", "text": "..."}] 形式の入力データ"""
    requests = []
    with MANIFEST.open("a", encoding="utf-8") as mf:
        for item in items:
            # attempt=1 を custom_id に埋め込む(再投入時に 2, 3 と上げる)
            custom_id = f"{item['id']}__a1"
            requests.append({
                "custom_id": custom_id,
                "params": {
                    "model": "claude-sonnet-5",
                    "max_tokens": 300,
                    "messages": [{
                        "role": "user",
                        "content": f"次の記事本文に合うタグを3つ、JSON配列で返してください:\n\n{item['text']}"
                    }],
                },
            })
            # 台帳: custom_id・元ID・入力ハッシュ・投入時刻を記録
            mf.write(json.dumps({
                "custom_id": custom_id,
                "source_id": item["id"],
                "input_sha": hashlib.sha256(item["text"].encode()).hexdigest()[:16],
                "attempt": 1,
            }, ensure_ascii=False) + "\n")
    return requests
 
batch = client.messages.batches.create(requests=build_requests(items))
print(f"batch_id={batch.id} submitted={len(items)}")

custom_id の末尾に __a1 と試行番号を付けているのが要点です。Anthropic 側は同一バッチ内の custom_id 重複を拒否しますが、別バッチ間の重複は関知しません。再投入分を同じ ID で投げると、結果を集約するときにどちらの応答か区別できなくなります。試行番号を ID に埋め込んでおけば、台帳側で「この source_id の最新 attempt はどれか」を一意に辿れます。

なお空文字列の本文をそのまま投げた失敗を経験してからは、build_requests の入口で if not item["text"].strip(): continue の門番を置き、弾いた件数も台帳に記録するようにしました。invalid_request はバッチに入る前に潰すのが一番安上がりです。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
custom_id 台帳と結果ストリームを突き合わせ、succeeded / errored / canceled / expired の内訳と欠落を機械的に検出する照合コード一式
attempt 番号付き custom_id で二重処理を防ぐ再投入設計と、リトライで直る失敗・直らない失敗を分ける判定基準の一覧表
Sonnet 5 の導入価格に Batch 50% 割引を重ねた実測コストと、24時間処理ウィンドウ・ポーリング間隔の実務的な目安
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

API & SDK2026-06-28
ストリーミング応答のCPUと取りこぼしを実測して長時間バッチを安定させる
夜間に走らせた長時間バッチが、朝には半分しか終わっていない。原因はCPU張り付きとストリームの中断でした。streamのCPU・スループットを実測し、中断から再開する監視ラッパーの実装を紹介します。
API & SDK2026-06-27
自己修復ループに「諦める条件」を設計する — エラーを4分類して再試行予算を割り当てる
LLMの自己修復ループは「直し続ければいつか通る」という幻想で破綻します。エラーを4つに分類し、クラスごとに再試行予算を引く設計を、動くTypeScriptと実測コストで解説します。
API & SDK2026-06-24
上限が倍になった日に決めたこと — 共有APIキーで定期ジョブを束ねる余白予算の設計
レート上限が倍になっても間隔を詰めなかった理由と、共有 API キーで複数の定期ジョブを束ねる『余白予算』の設計を、ヘッダー計測と実コードでまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →