Message Batches API: cut Claude costs by 50% for work that can wait
TL;DR
The Batch API accepts up to 100,000 requests (or 256 MB) per batch, billed at 50% of standard prices. Most batches finish in under an hour, at most 24 hours; results are kept for 29 days. It stacks with prompt caching.
Not every request you send to Claude needs an answer within seconds. Classifying 10,000 support tickets, summarising a document archive, running an eval suite across prompt versions, generating descriptions for thousands of products — all of this can wait. For that kind of work, the Message Batches API lets you submit a whole batch at once, processes it asynchronously, and charges just 50% of standard prices.
What is the Batches API?
It is the Messages API called in bulk: you bundle many requests into a batch, submit it, and come back for the results when processing ends. Each request is handled independently — one failed request doesn’t affect the rest. According to the official docs, most batches finish in under 1 hour.
Numbers to remember
| Item | Limit |
|---|---|
| Price | 50% of standard API prices, for both input and output |
| Max requests per batch | 100,000 requests |
| Max batch size | 256 MB (whichever limit is hit first applies; exceeding it returns 413 request_too_large) |
| Processing time | Mostly under 1 hour; up to 24 hours — anything left after that is expired |
| Result retention | 29 days from created_at (not from when it ended) |
custom_id | Unique within the batch, 1–64 letters, digits, _ or - |
Reference batch prices (USD per million tokens)
| Model | Input (batch) | Output (batch) |
|---|---|---|
| Claude Opus 5.5 | $2 | $10 |
| Claude Sonnet 5.5 | $1 | $5 |
| Claude Haiku 5.5 (prompts ≤100K tokens) | $0.05 | $0.25 |
Prices from the Claude Docs batch-processing page at the time of writing; always re-check before budgeting.
What is and isn’t supported
All active models support the Batches API. A single batch can mix request types, and nearly every Messages API feature works: vision, tool use (including server tools such as web search, web fetch, code execution and MCP connectors), system prompts, multi-turn conversations, extended thinking, most beta features, and prompt caching.
Parameters that fail validation:
stream: true— results come back as a file, not a stream.speed(fast mode) andfallbacks— only meaningful for the synchronous API.max_tokens: 0(cache pre-warming) — the cache would expire before a follow-up request could use it.
One nice bonus: with the beta header output-300k-2026-03-24, batch requests can raise max_tokens to 300,000 on supported models — available on the Batches API only, not the synchronous API.
The 4-step workflow (Python)
Step 1 — Create the batch
import anthropic
client = anthropic.Anthropic()
batch = client.messages.batches.create(requests=[
{
"custom_id": f"ticket-{t['id']}",
"params": {
"model": "claude-haiku-5-5",
"max_tokens": 200,
"system": "Classify the ticket as: billing, bug, feature, other. Return only the label.",
"messages": [{"role": "user", "content": t["text"]}],
},
}
for t in tickets
])
print(batch.id, batch.processing_status) # in_progress
Note: params are validated asynchronously — errors only show up once the batch ends. So dry-run one identical request through the regular Messages API before submitting the whole batch.
Step 2 — Poll for status
import time
while True:
b = client.messages.batches.retrieve(batch.id)
if b.processing_status == "ended":
break
print(b.request_counts) # processing / succeeded / errored / canceled / expired
time.sleep(60)
processing_status takes three values: in_progress, canceling and ended. You can also watch batches in the Claude Console.
Step 3 — Fetch the results
labels = {}
retry = []
for r in client.messages.batches.results(batch.id):
match r.result.type:
case "succeeded":
labels[r.custom_id] = r.result.message.content[0].text
case "errored":
if r.result.error.error.type == "invalid_request_error":
print("Fix request:", r.custom_id)
else:
retry.append(r.custom_id) # server error: safe to resend
case "expired" | "canceled":
retry.append(r.custom_id)
Results are a .jsonl file, one object per line with custom_id and result. Crucially, results don’t come back in submission order — always match them by custom_id.
| Result type | Meaning | Billed? |
|---|---|---|
succeeded | Success, includes the message | Yes |
errored | Invalid request or server error | No |
canceled | Cancelled before reaching the model | No |
expired | The 24-hour window ran out first | No |
Step 4 — Cancel or delete when needed
Call client.messages.batches.cancel(id) to cancel a running batch: its status moves to canceling, then ended, possibly with partial results. To delete batch data after processing, use DELETE /v1/messages/batches/{batch_id} (cancel an in-progress batch first).
Combining with prompt caching
Batch and caching discounts stack. Because batch requests run concurrently, cache hits are best-effort, typically between 30% and 98%. To raise the hit rate:
- Put identical
cache_controlblocks in every request (for example the system prompt plus reference documents). - Keep a steady request stream so 5-minute cache entries don’t expire mid-run.
- Structure requests to share as much content as possible; for long-running batches, consider the 1-hour cache.
When to use it — and when not to
| Good fit | Poor fit |
|---|---|
| Bulk classification, labelling, data extraction | Chatbots answering users in real time |
| Summarising or translating document archives | Multi-step agents that need each step’s output to continue |
| Evals, comparing many prompt versions | Work with second-level deadlines |
| Content moderation, high-volume generation | Requests that need token streaming |
Operational notes
- Split it up: break very large datasets into several batches so they’re easier to monitor and retry.
- Stream results: iterate over
results()instead of loading everything into memory. - Spend limits: because of the high throughput, a batch may slightly exceed the Workspace spend limit.
- Workspace scope: a batch belongs to the Workspace that created it; only API keys from that Workspace or Console users with permission can see it.
- Data: requests and responses are stored for up to 29 days for processing; delete proactively via the DELETE endpoint if the data is sensitive.
How to apply it
- Developers: Split your pipeline into two layers — real time (regular API) for users, and an overnight batch layer for analysis, labelling and reports. The bill drops noticeably while the user experience stays the same.
- Data/QA: Run thousand-case eval suites as batches every time a prompt changes — half the price and no pressure on your synchronous rate limits.
- PMs: When estimating AI costs for a feature, ask the team: “which part can wait a few hours?” Moving that part to batches saves 50% immediately.
Sources & references
- Batch processing — Claude Docs
- Message Batches API reference — Claude Docs