CLAUDE LABEN
SUNSET — 旧 Workbench と実験的プロンプトツール API の廃止は明日8月17日です。残り1日となりましたLATEST — 最新は8月15日の v2.1.233 です。GitLab のマージリクエスト URL が --worktree と claude agents で扱えるようになり、MR は !N 形式で表示されますSECURITY — Windows で NT の \??\ デバイスプレフィックスを含むパスが UNC 検証をすり抜ける問題が直りました。NTLM 資格情報が漏れる経路が塞がれていますTODO — Opus 4.8・Sonnet 5・Fable 5・Mythos 5 以降では TodoWrite と Task 系ツールが既定で無効になりました。CLAUDE_CODE_ENABLE_TODO_TOOLS=1 で戻せますFORK — 8月14日の v2.1.232 で subagent_type: 'fork' が既定になり、会話全体とプロンプトキャッシュをそのまま引き継げるようになっていますMENTION — プロンプトで @ を打つと別のセッションを名前で呼び出せます。SendMessage がそのセッションへ直接届きますSUNSET — 旧 Workbench と実験的プロンプトツール API の廃止は明日8月17日です。残り1日となりましたLATEST — 最新は8月15日の v2.1.233 です。GitLab のマージリクエスト URL が --worktree と claude agents で扱えるようになり、MR は !N 形式で表示されますSECURITY — Windows で NT の \??\ デバイスプレフィックスを含むパスが UNC 検証をすり抜ける問題が直りました。NTLM 資格情報が漏れる経路が塞がれていますTODO — Opus 4.8・Sonnet 5・Fable 5・Mythos 5 以降では TodoWrite と Task 系ツールが既定で無効になりました。CLAUDE_CODE_ENABLE_TODO_TOOLS=1 で戻せますFORK — 8月14日の v2.1.232 で subagent_type: 'fork' が既定になり、会話全体とプロンプトキャッシュをそのまま引き継げるようになっていますMENTION — プロンプトで @ を打つと別のセッションを名前で呼び出せます。SendMessage がそのセッションへ直接届きます
記事一覧/API & SDK
API & SDK/2026-07-02上級

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

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

Claude API117Message Batches2バッチ処理4エラーハンドリング7運用設計21

プレミアム記事

バッチの状態が 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-07-11
本番で観測した引数分布からツールスキーマを締め直す
Claudeのツール呼び出しが渡してくる引数を本番で丸ごと記録し、その分布からJSON Schemaにenumやpatternを足して締め直す運用手法を整理しました。緩いスキーマが生む意味のドリフトの実例、段階的に締める手順、締め直し前後の実測値、状況別の推奨まで計測コード付きで紹介します。
API & SDK2026-07-10
夜間バッチが勝手に上位モデルで走っていた — タスクごとにモデル上限を宣言して機械で守る
モデル名をコードに散らすと、いつのまにか安いはずのバッチが上位モデルで走ります。タスクごとの上限を宣言するマニフェストと、既定拒否の解決レイヤ、そして逸脱を検出する差分監査の設計をコード付きでまとめました。
API & SDK2026-07-04
Claude apps gateway の発表を読んで、個人開発の管制面を作り直した話
セルフホスト型 Claude apps gateway の設計を管制面と実行面の分離として読み解き、個人開発の規模に縮約します。アプリ別コスト帰属・モデル許可リスト・fail-closed の支出上限を Cloudflare Workers で実装した記録です。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →