◉CLAUDE LABEN
●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り45日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り45日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/API & SDK
⬡ API & SDK/2026-07-02上級

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

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

Claude API128Message Batchesバッチ処理4エラーハンドリング7運用設計28

✦ プレミアム記事

バッチの状態が 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-08-30
モデルIDを一括置換する前に、手元の CLI が抱えるカタログを引きます
新しい世代が出ると、前の世代のモデルIDが急に古く見えます。インストール済みの実行ファイルに埋め込まれたカタログを引き、一致・日付不一致・書式破損の三段に仕分けてから直す手順を、実測値とスクリプトつきでまとめました。
⬡ API & SDK2026-04-11
Claude API の Message Batches を夜間処理に回す — 割引50%の実像と、24時間という期限の設計
Claude API の Message Batches を実際に回して分かったこと。割引50%が効く範囲、custom_id の文字種制限、24時間の期限と29日の保持期間を踏まえた夜間バッチの組み方をまとめました。
⬡ API & SDK2026-10-16
Make から Claude API に 40 行流すと 14 行目で止まる — 429 を待ち時間の計算で避ける
表計算の行を1件ずつ Claude API に渡すノーコード自動化が、途中で 429 になる理由と直し方です。模擬バケットで7通りの間隔を比べ、Sleep の秒数を 60÷上限−応答時間 で決める式と、失敗行を落とさない設定を示します。