Halve the Bill with the Batch API
A 50% discount on anything that does not need an answer right now
The trade
Every major provider offers the same deal: submit your requests asynchronously, accept a slower turnaround, pay half. Input and output both.
There is no quality difference. Same models, same weights, same output. You are paying for latency, and batch says you do not need it.
What qualifies
Ask one question: *does a human need this answer in the next few seconds?*
If no, it is batch-eligible:
- Backfilling summaries or embeddings over an existing corpus
- Bulk classification, extraction, tagging
- Running an eval suite
- Nightly report generation
- Migration work: rewriting a thousand files to a new pattern
- Anything on a cron
If yes, it is not:
- An interactive coding agent
- Anything a user is waiting on
- A tool call inside a live agentic loop
The shape of a batch job
import anthropic
client = anthropic.Anthropic()
batch = client.messages.batches.create(
requests=[
{
"custom_id": f"ticket-{t['id']}",
"params": {
"model": "claude-haiku-4-5-20251001",
"max_tokens": 512,
"system": SHARED_PROMPT,
"messages": [{"role": "user", "content": t["body"]}],
},
}
for t in tickets
]
)
print(batch.id, batch.processing_status)
Then poll and collect:
import time
while True:
batch = client.messages.batches.retrieve(batch.id)
if batch.processing_status == "ended":
break
time.sleep(30)
for result in client.messages.batches.results(batch.id):
handle(result.custom_id, result.result)
custom_id is how you match results back to inputs. Results do not come back in order, always key on it, never on position.
It stacks with caching
Batch and caching are independent discounts and they compose. A bulk job with a large shared system prompt gets both:
base: $X + batch (50% off): $X × 0.5 + cached prefix reads: the input share drops to ~10%
The failure mode to plan for
Batches are not transactional. Individual requests can fail while the rest succeed, usually on token limits or malformed input.
for result in client.messages.batches.results(batch.id):
if result.result.type == "succeeded":
store(result.custom_id, result.result.message)
else:
retry_queue.append(result.custom_id)
Always inspect the result type. A pipeline that assumes every request succeeded will silently lose rows.
See also: Prompt Caching Economics · Choose the Right Model