
Process 100,000 Claude Requests in One Call: The Message Batches API for Bulk Async Jobs
Chris Harper
3 min read
Jul 14, 2026 · 12:05 UTC
What you'll be able to do after this:
- Submit thousands of Claude API requests in a single async batch with no rate-limit or retry machinery to write
- Get a guaranteed 50% cost reduction on every input and output token in the batch
- Reconcile results back to your original requests with stable custom IDs, and handle partial failures cleanly
TL;DR: The Message Batches API sends up to 100,000 Claude requests as one async job at half price — the right tool for eval harnesses, data labeling, and any pipeline that does not need real-time responses.
When you have 10,000 documents to classify, 5,000 support tickets to route, or an eval harness to run on a fresh fine-tune, the synchronous API works but costs money you do not need to spend and forces you to write retry and rate-limit machinery. The Message Batches API collapses all of that: submit once, poll for completion, stream results.
The three numbers that matter
- Up to 100,000 requests per batch (256 MB total limit)
- 50% cost on both input and output tokens — all Claude models, no configuration required
- Results within 24 hours (most batches finish in under an hour)
Submit a batch in 15 lines
import anthropic
client = anthropic.Anthropic()
batch = client.messages.batches.create(
requests=[
{
"custom_id": f"doc-{i}",
"params": {
"model": "claude-sonnet-5",
"max_tokens": 50,
"messages": [{"role": "user", "content": f"Classify as bug/feature/question: {doc}"}],
},
}
for i, doc in enumerate(documents)
]
)
print(batch.id, batch.processing_status) # ended | in_progress | validating
custom_id is your key for reconciliation — use something that maps back to your source data.
Poll until done, then stream results
import time
while True:
status = client.messages.batches.retrieve(batch.id)
if status.processing_status == "ended":
break
time.sleep(60) # most batches finish in under 30 minutes
for result in client.messages.batches.results(batch.id):
if result.result.type == "succeeded":
label = result.result.message.content[0].text
print(f"{result.custom_id}: {label}")
elif result.result.type == "errored":
print(f"{result.custom_id}: failed — resubmit")
When to use batches vs. real-time
| Scenario | Use |
|---|---|
| User waiting on screen | Synchronous Messages API |
| Eval harness, bulk annotation, nightly pipeline | Batch API |
| Agent in an interactive loop | Synchronous Messages API |
| Annotating a fine-tuning dataset | Batch API |
Power combo: batches + prompt caching
Mark your system prompt with "cache_control": {"type": "ephemeral"} on every request in the batch. Cached tokens get the 0.1x cache rate AND the 0.5x batch rate — effectively 5% of standard input cost on the prefix. For large system prompts or long retrieval contexts repeated across every item, this can cut total batch cost by 90%+.
Partial failures are normal
Each result has a type: "succeeded", "errored", or "expired" (rare: after 24 h without completion). Collect the non-success custom_ids and resubmit them as a smaller follow-up batch. The batch API does not retry internally — that is by design so you control the failure policy.
Sources: Batch processing — Claude Platform Docs · Batch processing cookbook — Claude Cookbook · Message Batches API reference